mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
fix: resume unfinished work after repository preparation (#17047)
## Thinking Path
> - Paperclip manages agents that perform work for a company.
> - Task repositories are prepared at normal run admission.
> - A running native task can stage a repository while its work remains
unfinished.
> - The controller did not offer that saved setup as a continuation.
> - The agent could add a timer or wait for a user restart.
> - This change uses the existing same-agent continuation and status
ledger.
> - The accepted explanation and proven conversation remain available
after setup.
## Linked Issues or Issue Description
Refs #17045. That prerequisite has merged. This PR targets master.
**What happened?**
A projectless native task can request a repository for the next
admission. An unfinished task has no server-selected repository
continuation. An explicit same-agent continuation also loses chat
provenance.
**Expected behavior**
An unfinished standard task can yield and resume through the existing
admission queue. Its explanation and final result retain the authorized
conversation. A task that only stages a repository can complete without
another run.
**Steps to reproduce**
1. Start a native standard task in a task directory.
2. Request an authorized repository through prepare_repository.
3. Yield with unfinished work and no explicit continuation.
4. Observe that the former driver rejects the yield, although the
pending repository is saved.
## What Changed
- Offer a saved same-agent continuation for the current task's pending
repository.
- Accept that current server continuation in the shared provider driver.
- Resolve chat provenance through applied status decisions, delivered
effects, and admitted wakes.
- Preserve accepted setup explanations without repeating an owned
question card.
- Document optional reproduction details and configured repository
discovery.
- Extend database regressions and workspace access maps.
- Preserve Copilot profile v52 and bind the shared continuation source
in v53. Include the active identity in the QA image fingerprint.
## Verification
- Five focused server suites: 100 checks pass.
- Shared continuation driver: 12 checks pass.
- Server typecheck passes.
- Map contract and whitespace checks pass.
- Existing API and driver assertions fail on the former implementation.
- Real dispatcher and queue cohort: 43 checks pass. The original
reply-loss regression fails before the dispatch receipt fix.
- Continuation driver and installation identity cohort: 62 checks pass.
- Regenerated capability inventory and contract checks pass.
- Final response precedence cohort: 109 checks pass. The stale-comment
case fails on the former implementation.
- All required CI checks, build, and both source reviews pass on
b9c8677635.
- Live provider journeys and deployed acceptance remain pending. No live
qualification is claimed.
## Risks
A pending repository now offers a next admission when unfinished
standard work yields. Incorrect eligibility could create unwanted work.
The lookup requires the current workspace and task. Explicit completion
does not use it. Existing Stop, admission, approval, and transport
authorization remain authoritative. No schema or public task-status
change is included. The shared continuation change requires a new
Copilot source identity. This PR preserves v52 history and does not
claim fresh Copilot live qualification.
## Model Used
OpenAI Codex, GPT-6 family, with repository tools and code execution.
The runtime does not expose its exact deployment ID or context-window
size. Those values are not inferred.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
8b873e0112
commit
dff63551fe
32 files changed
+660
-279
No files matched your search
@@ -153,6 +153,13 @@ and the matching server-issued scheduling receipt on the source run. Company,
|
||||
task, agent, requester, schedule identity, and current delivery authorization
|
||||
remain enforced. Provider-supplied source IDs do not establish that lineage.
|
||||
|
||||
An accepted same-agent continuation retains the same presentation contract.
|
||||
Its successor inherits conversation provenance only through the applied status
|
||||
decision, delivered continuation effect, and admitted wake that name the same
|
||||
company, task, agent, and requester. Dispatch still checks current endpoint,
|
||||
principal, and resource authorization. These receipts do not grant execution
|
||||
or override Stop.
|
||||
|
||||
A successful Slack turn with a published final reply and no remaining execution
|
||||
or decision path settles to `chat_conversations.state = waiting` and issue
|
||||
`in_review`. This is a server-owned passive conversation state, displayed as
|
||||
@@ -509,9 +516,13 @@ of the invalid call. Unauthorized tools still receive a generic rejection, and
|
||||
corrected calls retain the same permission and approval checks.
|
||||
|
||||
A native provider may omit `continuation` (or send `null`) when it reports
|
||||
`yielded` after saving a monitor. The controller reads current authorized task
|
||||
context and fills the canonical `monitor` continuation only if that same task,
|
||||
run, assignee, work mode, and monitor remain eligible. Completion admission
|
||||
`yielded` after saving a monitor or requesting repository preparation. The controller reads current authorized task
|
||||
context and fills the canonical continuation only if that same task,
|
||||
run, assignee, and work mode remain eligible. An eligible monitor takes precedence.
|
||||
A pending repository requested by that task in its current workspace offers
|
||||
`same_agent`; the existing status arbiter and wake queue own the next admission.
|
||||
Prepared repositories and requests belonging to another task cannot offer it.
|
||||
An explicit `done` result does not acquire a continuation. Completion admission
|
||||
checks that persisted monitor again. This default does not create or renew a
|
||||
schedule. A consumed, cleared, exhausted, or ineligible monitor cannot authorize
|
||||
a passive wait. An explicit `response_wake` still identifies a real response
|
||||
|
||||
@@ -21,7 +21,7 @@ selection, reusing the existing campaign accounting and reporting pipeline.
|
||||
| Yielded monitor presentation | Explanation retained; no false completion or duplicate question | Repair and focused checks pass |
|
||||
| Monitor conversation lineage | Exact scheduling receipt; current authorization; no unrelated channel disclosure | Repair and database checks pass |
|
||||
| Free-text question card | Prompt, context, and Respond visible; answers stay private | Repair and database checks pass |
|
||||
| Repository preparation | Authorized context; automatic successor; retained workspace and tools | Inspecting existing repairs |
|
||||
| Repository preparation | Authorized context; automatic successor; retained workspace and tools | Same-agent continuation repair and composed regressions pass; live proof pending |
|
||||
| Finalization recovery | Saved result survives; zero repeated completed provider turns | Saved-summary repair and database checks pass |
|
||||
| Migration and follow-ups | Configuration retained and subsequent work delivered | Qualification pending |
|
||||
|
||||
@@ -46,9 +46,10 @@ the presence of a fixture.
|
||||
## Baseline
|
||||
|
||||
Source base: `8b4ab65b1c3d863d282752e83307943720a5c85c`.
|
||||
Private incident evidence stays outside the public repository. Serving revision,
|
||||
runner artifact, and historical successful deployment still require independent
|
||||
capture. Workspace and lifecycle changes identify changed execution paths, not
|
||||
Private incident evidence stays outside the public repository. The serving
|
||||
container build record independently matches the source base above. Runner
|
||||
artifact and historical successful deployment still require independent capture.
|
||||
Workspace and lifecycle changes identify changed execution paths, not
|
||||
an established single introducing cause. The earlier staging settings regression
|
||||
already has a merged repair; verify it rather than duplicating that change.
|
||||
|
||||
@@ -61,3 +62,11 @@ The repaired six-file cohort passes 148 checks against real disposable PostgreSQ
|
||||
including existing monitor scheduling, revocation, and recovery boundaries.
|
||||
Workspace typecheck passes. Full build, full suite, review, and live qualification
|
||||
remain separate gates. No deployed acceptance is claimed here.
|
||||
|
||||
The repository repair adds current-task pending-repository eligibility and uses
|
||||
the existing same-agent status decision and wake queue. Five composed server
|
||||
suites pass 100 checks, and the shared provider continuation driver passes 12.
|
||||
Server typecheck passes. These results include long conversation lineages,
|
||||
cycles, broken ledger receipts, current-task repository ownership, and completed
|
||||
repository rejection. Explicit completion and review modes remain distinct from
|
||||
an unfinished standard-task yield. They do not establish the live matrix.
|
||||
@@ -158,6 +158,14 @@ new controller-side Git acquisition. Evidence remains source-only here; focused
|
||||
[repository tests](../../server/src/__tests__/task-workspace-repositories-trust.test.ts)
|
||||
are separate from live agent qualification.
|
||||
|
||||
An admitted native standard task can read a same-agent completion continuation
|
||||
for its own pending repository in its current workspace. An explicit yield may
|
||||
use that saved continuation; explicit completion remains completion. Existing
|
||||
status decisions and wake admission retain Stop and authorization gates. The
|
||||
[workspace API regression](../../server/src/__tests__/task-workspace-api.test.ts)
|
||||
and [driver regression](../../packages/paperclip-runner/src/drivers/completion-continuation.test.ts)
|
||||
cover this contract; live qualification remains separate.
|
||||
|
||||
Policy: Company/project/workspace visibility and runtime authority; active run, trust
|
||||
preset and task ownership where required.
|
||||
|
||||
|
||||
@@ -94,6 +94,14 @@ preparation API to add an authorized repository beneath that workspace;
|
||||
repeat the request and verify that it reuses the checkout and pinned revision.
|
||||
See [task binding tests](../../server/src/__tests__/task-workspace-binding.test.ts)
|
||||
and [repository access tests](../../server/src/__tests__/task-workspace-repositories-trust.test.ts).
|
||||
|
||||
For native unfinished work, yield after preparation is staged and verify that
|
||||
normal admission prepares the repository and continues the same task without
|
||||
a timer or user message. A staging-only task that finishes must not wake again.
|
||||
The saved continuation must disappear when the repository is ready or belongs
|
||||
to another task. Source regressions cover those eligibility checks; a live
|
||||
provider journey is required to qualify the complete automatic resume.
|
||||
|
||||
An existing projectless folder alias of a sandbox-restricted project must be
|
||||
rejected for a standard local task before its adapter executes. Repeat after
|
||||
relocating the configured source; retained ownership must still enforce the
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"copilot": {
|
||||
"agentProfileVersion": 52,
|
||||
"agentProfileVersion": 53,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:3614db52d3ddcbc0153c79361a0db2c4324249475d3be490f152b64f916bdd53",
|
||||
"commandDigest": "sha256:6441ff7f3fc664064bff9e7ae80252ddec7a39236f9a4b9d402785eb8b03b2c9",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"claude": {
|
||||
|
||||
@@ -47,34 +47,34 @@ The skill/reference inventory and eval cases are the only normative behavior sou
|
||||
| skill:skills/paperclip/SKILL.md:conversation-tasks:32 | optional_agent_tool | skills/paperclip/SKILL.md:32 |
|
||||
| skill:skills/paperclip/SKILL.md:server-verified-external-chat-turns:49 | control_plane_owned | skills/paperclip/SKILL.md:49 |
|
||||
| skill:skills/paperclip/SKILL.md:the-heartbeat-procedure:89 | optional_agent_tool | skills/paperclip/SKILL.md:89 |
|
||||
| skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:200 | always_agent_tool | skills/paperclip/SKILL.md:200 |
|
||||
| skill:skills/paperclip/SKILL.md:status-quick-guide:248 | control_plane_owned | skills/paperclip/SKILL.md:248 |
|
||||
| skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:258 | optional_agent_tool | skills/paperclip/SKILL.md:258 |
|
||||
| skill:skills/paperclip/SKILL.md:delegating-review-tasks:271 | always_agent_tool | skills/paperclip/SKILL.md:271 |
|
||||
| skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:282 | control_plane_owned | skills/paperclip/SKILL.md:282 |
|
||||
| skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:290 | control_plane_owned | skills/paperclip/SKILL.md:290 |
|
||||
| skill:skills/paperclip/SKILL.md:requesting-board-approval:315 | optional_agent_tool | skills/paperclip/SKILL.md:315 |
|
||||
| skill:skills/paperclip/SKILL.md:issue-thread-interactions:336 | optional_agent_tool | skills/paperclip/SKILL.md:336 |
|
||||
| skill:skills/paperclip/SKILL.md:standalone-decisions:366 | optional_agent_tool | skills/paperclip/SKILL.md:366 |
|
||||
| skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:470 | optional_agent_tool | skills/paperclip/SKILL.md:470 |
|
||||
| skill:skills/paperclip/SKILL.md:niche-workflow-pointers:512 | optional_agent_tool | skills/paperclip/SKILL.md:512 |
|
||||
| skill:skills/paperclip/SKILL.md:cases:522 | optional_agent_tool | skills/paperclip/SKILL.md:522 |
|
||||
| skill:skills/paperclip/SKILL.md:company-skills-workflow:527 | optional_agent_tool | skills/paperclip/SKILL.md:527 |
|
||||
| skill:skills/paperclip/SKILL.md:routines:538 | optional_agent_tool | skills/paperclip/SKILL.md:538 |
|
||||
| skill:skills/paperclip/SKILL.md:task-files-and-workspace-choices:549 | optional_agent_tool | skills/paperclip/SKILL.md:549 |
|
||||
| skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:555 | optional_agent_tool | skills/paperclip/SKILL.md:555 |
|
||||
| skill:skills/paperclip/SKILL.md:proposing-credentials-safely:562 | optional_agent_tool | skills/paperclip/SKILL.md:562 |
|
||||
| skill:skills/paperclip/SKILL.md:reading-granted-secrets:569 | optional_agent_tool | skills/paperclip/SKILL.md:569 |
|
||||
| skill:skills/paperclip/SKILL.md:critical-rules:595 | optional_agent_tool | skills/paperclip/SKILL.md:595 |
|
||||
| skill:skills/paperclip/SKILL.md:comment-style-required:616 | always_agent_tool | skills/paperclip/SKILL.md:616 |
|
||||
| skill:skills/paperclip/SKILL.md:update:648 | optional_agent_tool | skills/paperclip/SKILL.md:648 |
|
||||
| skill:skills/paperclip/SKILL.md:task-documents-and-deliverables:658 | always_agent_tool | skills/paperclip/SKILL.md:658 |
|
||||
| skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:672 | optional_agent_tool | skills/paperclip/SKILL.md:672 |
|
||||
| skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:703 | optional_agent_tool | skills/paperclip/SKILL.md:703 |
|
||||
| skill:skills/paperclip/SKILL.md:searching-issues:733 | optional_agent_tool | skills/paperclip/SKILL.md:733 |
|
||||
| skill:skills/paperclip/SKILL.md:full-reference:743 | optional_agent_tool | skills/paperclip/SKILL.md:743 |
|
||||
| skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:747 | always_agent_tool | skills/paperclip/SKILL.md:747 |
|
||||
| skill:skills/paperclip/SKILL.md:incidental-feedback:764 | optional_agent_tool | skills/paperclip/SKILL.md:764 |
|
||||
| skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:201 | always_agent_tool | skills/paperclip/SKILL.md:201 |
|
||||
| skill:skills/paperclip/SKILL.md:status-quick-guide:249 | control_plane_owned | skills/paperclip/SKILL.md:249 |
|
||||
| skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:259 | optional_agent_tool | skills/paperclip/SKILL.md:259 |
|
||||
| skill:skills/paperclip/SKILL.md:delegating-review-tasks:272 | always_agent_tool | skills/paperclip/SKILL.md:272 |
|
||||
| skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:283 | control_plane_owned | skills/paperclip/SKILL.md:283 |
|
||||
| skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:291 | control_plane_owned | skills/paperclip/SKILL.md:291 |
|
||||
| skill:skills/paperclip/SKILL.md:requesting-board-approval:316 | optional_agent_tool | skills/paperclip/SKILL.md:316 |
|
||||
| skill:skills/paperclip/SKILL.md:issue-thread-interactions:337 | optional_agent_tool | skills/paperclip/SKILL.md:337 |
|
||||
| skill:skills/paperclip/SKILL.md:standalone-decisions:367 | optional_agent_tool | skills/paperclip/SKILL.md:367 |
|
||||
| skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:471 | optional_agent_tool | skills/paperclip/SKILL.md:471 |
|
||||
| skill:skills/paperclip/SKILL.md:niche-workflow-pointers:513 | optional_agent_tool | skills/paperclip/SKILL.md:513 |
|
||||
| skill:skills/paperclip/SKILL.md:cases:523 | optional_agent_tool | skills/paperclip/SKILL.md:523 |
|
||||
| skill:skills/paperclip/SKILL.md:company-skills-workflow:528 | optional_agent_tool | skills/paperclip/SKILL.md:528 |
|
||||
| skill:skills/paperclip/SKILL.md:routines:539 | optional_agent_tool | skills/paperclip/SKILL.md:539 |
|
||||
| skill:skills/paperclip/SKILL.md:task-files-and-workspace-choices:550 | optional_agent_tool | skills/paperclip/SKILL.md:550 |
|
||||
| skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:558 | optional_agent_tool | skills/paperclip/SKILL.md:558 |
|
||||
| skill:skills/paperclip/SKILL.md:proposing-credentials-safely:565 | optional_agent_tool | skills/paperclip/SKILL.md:565 |
|
||||
| skill:skills/paperclip/SKILL.md:reading-granted-secrets:572 | optional_agent_tool | skills/paperclip/SKILL.md:572 |
|
||||
| skill:skills/paperclip/SKILL.md:critical-rules:598 | optional_agent_tool | skills/paperclip/SKILL.md:598 |
|
||||
| skill:skills/paperclip/SKILL.md:comment-style-required:619 | always_agent_tool | skills/paperclip/SKILL.md:619 |
|
||||
| skill:skills/paperclip/SKILL.md:update:651 | optional_agent_tool | skills/paperclip/SKILL.md:651 |
|
||||
| skill:skills/paperclip/SKILL.md:task-documents-and-deliverables:661 | always_agent_tool | skills/paperclip/SKILL.md:661 |
|
||||
| skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:675 | optional_agent_tool | skills/paperclip/SKILL.md:675 |
|
||||
| skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:706 | optional_agent_tool | skills/paperclip/SKILL.md:706 |
|
||||
| skill:skills/paperclip/SKILL.md:searching-issues:736 | optional_agent_tool | skills/paperclip/SKILL.md:736 |
|
||||
| skill:skills/paperclip/SKILL.md:full-reference:746 | optional_agent_tool | skills/paperclip/SKILL.md:746 |
|
||||
| skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:750 | always_agent_tool | skills/paperclip/SKILL.md:750 |
|
||||
| skill:skills/paperclip/SKILL.md:incidental-feedback:767 | optional_agent_tool | skills/paperclip/SKILL.md:767 |
|
||||
| skill:skills/paperclip/references/api-reference.md:paperclip-api-reference:1 | optional_agent_tool | skills/paperclip/references/api-reference.md:1 |
|
||||
| skill:skills/paperclip/references/api-reference.md:response-schemas:9 | optional_agent_tool | skills/paperclip/references/api-reference.md:9 |
|
||||
| skill:skills/paperclip/references/api-reference.md:agent-record-get-api-agents-me-or-get-api-agents-agentid:11 | optional_agent_tool | skills/paperclip/references/api-reference.md:11 |
|
||||
|
||||
@@ -56,252 +56,252 @@
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:200",
|
||||
"id": "skill:skills/paperclip/SKILL.md:201",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L200:generated-artifacts-and-work-products",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L201:generated-artifacts-and-work-products",
|
||||
"heading": "Generated Artifacts and Work Products",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"semanticOperation": "register_deliverable",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:248",
|
||||
"id": "skill:skills/paperclip/SKILL.md:249",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L248:status-quick-guide",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L249:status-quick-guide",
|
||||
"heading": "Status Quick Guide",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:258",
|
||||
"id": "skill:skills/paperclip/SKILL.md:259",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L258:monitors-and-watchers-say-only-what-you-actually-scheduled",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L259:monitors-and-watchers-say-only-what-you-actually-scheduled",
|
||||
"heading": "Monitors and Watchers (say only what you actually scheduled)",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:271",
|
||||
"id": "skill:skills/paperclip/SKILL.md:272",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L271:delegating-review-tasks",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L272:delegating-review-tasks",
|
||||
"heading": "Delegating review tasks",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:282",
|
||||
"id": "skill:skills/paperclip/SKILL.md:283",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L282:managing-a-user-s-inbox",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L283:managing-a-user-s-inbox",
|
||||
"heading": "Managing A User's Inbox",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:290",
|
||||
"id": "skill:skills/paperclip/SKILL.md:291",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L290:issue-dependencies-blockers",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L291:issue-dependencies-blockers",
|
||||
"heading": "Issue Dependencies (Blockers)",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:315",
|
||||
"id": "skill:skills/paperclip/SKILL.md:316",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L315:requesting-board-approval",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L316:requesting-board-approval",
|
||||
"heading": "Requesting Board Approval",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:336",
|
||||
"id": "skill:skills/paperclip/SKILL.md:337",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L336:issue-thread-interactions",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L337:issue-thread-interactions",
|
||||
"heading": "Issue-Thread Interactions",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"semanticOperation": "request_human_input",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:366",
|
||||
"id": "skill:skills/paperclip/SKILL.md:367",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L366:standalone-decisions",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L367:standalone-decisions",
|
||||
"heading": "Standalone Decisions",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:470",
|
||||
"id": "skill:skills/paperclip/SKILL.md:471",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L470:mcp-tool-approval-gates",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L471:mcp-tool-approval-gates",
|
||||
"heading": "MCP Tool Approval Gates",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:512",
|
||||
"id": "skill:skills/paperclip/SKILL.md:513",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L512:niche-workflow-pointers",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L513:niche-workflow-pointers",
|
||||
"heading": "Niche Workflow Pointers",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:522",
|
||||
"id": "skill:skills/paperclip/SKILL.md:523",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L522:cases",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L523:cases",
|
||||
"heading": "Cases",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:527",
|
||||
"id": "skill:skills/paperclip/SKILL.md:528",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L527:company-skills-workflow",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L528:company-skills-workflow",
|
||||
"heading": "Company Skills Workflow",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:538",
|
||||
"id": "skill:skills/paperclip/SKILL.md:539",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L538:routines",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L539:routines",
|
||||
"heading": "Routines",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:549",
|
||||
"id": "skill:skills/paperclip/SKILL.md:550",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L549:task-files-and-workspace-choices",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L550:task-files-and-workspace-choices",
|
||||
"heading": "Task Files and Workspace Choices",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:555",
|
||||
"id": "skill:skills/paperclip/SKILL.md:558",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L555:issue-workspace-runtime-controls",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L558:issue-workspace-runtime-controls",
|
||||
"heading": "Issue Workspace Runtime Controls",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:562",
|
||||
"id": "skill:skills/paperclip/SKILL.md:565",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L562:proposing-credentials-safely",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L565:proposing-credentials-safely",
|
||||
"heading": "Proposing Credentials Safely",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:569",
|
||||
"id": "skill:skills/paperclip/SKILL.md:572",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L569:reading-granted-secrets",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L572:reading-granted-secrets",
|
||||
"heading": "Reading Granted Secrets",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:595",
|
||||
"id": "skill:skills/paperclip/SKILL.md:598",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L595:critical-rules",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L598:critical-rules",
|
||||
"heading": "Critical Rules",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:616",
|
||||
"id": "skill:skills/paperclip/SKILL.md:619",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L616:comment-style-required",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L619:comment-style-required",
|
||||
"heading": "Comment Style (Required)",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"semanticOperation": "report_progress",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:648",
|
||||
"id": "skill:skills/paperclip/SKILL.md:651",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L648:update",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L651:update",
|
||||
"heading": "Update",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:658",
|
||||
"id": "skill:skills/paperclip/SKILL.md:661",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L658:task-documents-and-deliverables",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L661:task-documents-and-deliverables",
|
||||
"heading": "Task Documents and Deliverables",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"semanticOperation": "write_document",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:672",
|
||||
"id": "skill:skills/paperclip/SKILL.md:675",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L672:planning-required-when-planning-requested",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L675:planning-required-when-planning-requested",
|
||||
"heading": "Planning (Required when planning requested)",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"semanticOperation": "write_document",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:703",
|
||||
"id": "skill:skills/paperclip/SKILL.md:706",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L703:key-endpoints-hot-routes",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L706:key-endpoints-hot-routes",
|
||||
"heading": "Key Endpoints (Hot Routes)",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:733",
|
||||
"id": "skill:skills/paperclip/SKILL.md:736",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L733:searching-issues",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L736:searching-issues",
|
||||
"heading": "Searching Issues",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:743",
|
||||
"id": "skill:skills/paperclip/SKILL.md:746",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L743:full-reference",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L746:full-reference",
|
||||
"heading": "Full Reference",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:747",
|
||||
"id": "skill:skills/paperclip/SKILL.md:750",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L747:conversational-confirmation-answers",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L750:conversational-confirmation-answers",
|
||||
"heading": "Conversational confirmation answers",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"semanticOperation": "call_api",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:764",
|
||||
"id": "skill:skills/paperclip/SKILL.md:767",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L764:incidental-feedback",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L767:incidental-feedback",
|
||||
"heading": "Incidental feedback",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
|
||||
@@ -5,6 +5,6 @@ Generated by `scripts/generate-capability-contract.mjs`; do not edit generated f
|
||||
- Skill/reference headings: 167
|
||||
- Legacy MCP tools: 42
|
||||
- Eval cases: 106 across 16 groups
|
||||
- Deterministic content SHA-256: `56f834a342a1e75e535d1f1a23f483882331d1fb364aa6bd17de7d9a2da39c4b`
|
||||
- Deterministic content SHA-256: `05f5724a98ae5e6045a550d969599c3e5f40d4c97069ae2453cc6e7e60d2ba44`
|
||||
|
||||
Every row has exactly one primary disposition, a source anchor, a semantic operation, and a mock-state expectation.
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: None,
|
||||
agent_runtime_version: None,
|
||||
command_digest:
|
||||
"sha256:3614db52d3ddcbc0153c79361a0db2c4324249475d3be490f152b64f916bdd53",
|
||||
"sha256:6441ff7f3fc664064bff9e7ae80252ddec7a39236f9a4b9d402785eb8b03b2c9",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"claude" => AcpxReleaseProfile {
|
||||
|
||||
@@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url";
|
||||
import { resolve, dirname } from "node:path";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const fixture = resolve(root, "test/fixtures/copilot-profile-v52-identity.json");
|
||||
const fixture = resolve(root, "test/fixtures/copilot-profile-v53-identity.json");
|
||||
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
|
||||
const declaration = { ...historical.declaration, agentProfileVersion: 52 };
|
||||
const declaration = { ...historical.declaration, agentProfileVersion: 53 };
|
||||
delete declaration.permissionIdentitySourceSha256;
|
||||
const sources = {
|
||||
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
|
||||
|
||||
@@ -105,9 +105,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:200",
|
||||
"id": "skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:201",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:200",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:201",
|
||||
"title": "Generated Artifacts and Work Products",
|
||||
"expectedSemantics": "Skill guidance headed “Generated Artifacts and Work Products”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -116,13 +116,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:200"
|
||||
"skill:skills/paperclip/SKILL.md:201"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:status-quick-guide:248",
|
||||
"id": "skill:skills/paperclip/SKILL.md:status-quick-guide:249",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:248",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:249",
|
||||
"title": "Status Quick Guide",
|
||||
"expectedSemantics": "Skill guidance headed “Status Quick Guide”.",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
@@ -131,13 +131,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:248"
|
||||
"skill:skills/paperclip/SKILL.md:249"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:258",
|
||||
"id": "skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:259",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:258",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:259",
|
||||
"title": "Monitors and Watchers (say only what you actually scheduled)",
|
||||
"expectedSemantics": "Skill guidance headed “Monitors and Watchers (say only what you actually scheduled)”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -146,13 +146,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:258"
|
||||
"skill:skills/paperclip/SKILL.md:259"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:delegating-review-tasks:271",
|
||||
"id": "skill:skills/paperclip/SKILL.md:delegating-review-tasks:272",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:271",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:272",
|
||||
"title": "Delegating review tasks",
|
||||
"expectedSemantics": "Skill guidance headed “Delegating review tasks”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -161,13 +161,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:271"
|
||||
"skill:skills/paperclip/SKILL.md:272"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:282",
|
||||
"id": "skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:283",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:282",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:283",
|
||||
"title": "Managing A User's Inbox",
|
||||
"expectedSemantics": "Skill guidance headed “Managing A User's Inbox”.",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
@@ -176,13 +176,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:282"
|
||||
"skill:skills/paperclip/SKILL.md:283"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:290",
|
||||
"id": "skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:291",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:290",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:291",
|
||||
"title": "Issue Dependencies (Blockers)",
|
||||
"expectedSemantics": "Skill guidance headed “Issue Dependencies (Blockers)”.",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
@@ -191,13 +191,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:290"
|
||||
"skill:skills/paperclip/SKILL.md:291"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:requesting-board-approval:315",
|
||||
"id": "skill:skills/paperclip/SKILL.md:requesting-board-approval:316",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:315",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:316",
|
||||
"title": "Requesting Board Approval",
|
||||
"expectedSemantics": "Skill guidance headed “Requesting Board Approval”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -206,13 +206,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:315"
|
||||
"skill:skills/paperclip/SKILL.md:316"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:issue-thread-interactions:336",
|
||||
"id": "skill:skills/paperclip/SKILL.md:issue-thread-interactions:337",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:336",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:337",
|
||||
"title": "Issue-Thread Interactions",
|
||||
"expectedSemantics": "Skill guidance headed “Issue-Thread Interactions”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -221,13 +221,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:336"
|
||||
"skill:skills/paperclip/SKILL.md:337"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:standalone-decisions:366",
|
||||
"id": "skill:skills/paperclip/SKILL.md:standalone-decisions:367",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:366",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:367",
|
||||
"title": "Standalone Decisions",
|
||||
"expectedSemantics": "Skill guidance headed “Standalone Decisions”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -236,13 +236,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:366"
|
||||
"skill:skills/paperclip/SKILL.md:367"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:470",
|
||||
"id": "skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:471",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:470",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:471",
|
||||
"title": "MCP Tool Approval Gates",
|
||||
"expectedSemantics": "Skill guidance headed “MCP Tool Approval Gates”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -251,13 +251,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:470"
|
||||
"skill:skills/paperclip/SKILL.md:471"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:niche-workflow-pointers:512",
|
||||
"id": "skill:skills/paperclip/SKILL.md:niche-workflow-pointers:513",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:512",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:513",
|
||||
"title": "Niche Workflow Pointers",
|
||||
"expectedSemantics": "Skill guidance headed “Niche Workflow Pointers”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -266,13 +266,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:512"
|
||||
"skill:skills/paperclip/SKILL.md:513"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:cases:522",
|
||||
"id": "skill:skills/paperclip/SKILL.md:cases:523",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:522",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:523",
|
||||
"title": "Cases",
|
||||
"expectedSemantics": "Skill guidance headed “Cases”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -281,13 +281,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:522"
|
||||
"skill:skills/paperclip/SKILL.md:523"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:company-skills-workflow:527",
|
||||
"id": "skill:skills/paperclip/SKILL.md:company-skills-workflow:528",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:527",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:528",
|
||||
"title": "Company Skills Workflow",
|
||||
"expectedSemantics": "Skill guidance headed “Company Skills Workflow”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -296,13 +296,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:527"
|
||||
"skill:skills/paperclip/SKILL.md:528"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:routines:538",
|
||||
"id": "skill:skills/paperclip/SKILL.md:routines:539",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:538",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:539",
|
||||
"title": "Routines",
|
||||
"expectedSemantics": "Skill guidance headed “Routines”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -311,13 +311,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:538"
|
||||
"skill:skills/paperclip/SKILL.md:539"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:task-files-and-workspace-choices:549",
|
||||
"id": "skill:skills/paperclip/SKILL.md:task-files-and-workspace-choices:550",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:549",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:550",
|
||||
"title": "Task Files and Workspace Choices",
|
||||
"expectedSemantics": "Skill guidance headed “Task Files and Workspace Choices”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -326,13 +326,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:549"
|
||||
"skill:skills/paperclip/SKILL.md:550"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:555",
|
||||
"id": "skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:558",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:555",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:558",
|
||||
"title": "Issue Workspace Runtime Controls",
|
||||
"expectedSemantics": "Skill guidance headed “Issue Workspace Runtime Controls”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -341,13 +341,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:555"
|
||||
"skill:skills/paperclip/SKILL.md:558"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:proposing-credentials-safely:562",
|
||||
"id": "skill:skills/paperclip/SKILL.md:proposing-credentials-safely:565",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:562",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:565",
|
||||
"title": "Proposing Credentials Safely",
|
||||
"expectedSemantics": "Skill guidance headed “Proposing Credentials Safely”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -356,13 +356,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:562"
|
||||
"skill:skills/paperclip/SKILL.md:565"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:reading-granted-secrets:569",
|
||||
"id": "skill:skills/paperclip/SKILL.md:reading-granted-secrets:572",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:569",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:572",
|
||||
"title": "Reading Granted Secrets",
|
||||
"expectedSemantics": "Skill guidance headed “Reading Granted Secrets”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -371,13 +371,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:569"
|
||||
"skill:skills/paperclip/SKILL.md:572"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:critical-rules:595",
|
||||
"id": "skill:skills/paperclip/SKILL.md:critical-rules:598",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:595",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:598",
|
||||
"title": "Critical Rules",
|
||||
"expectedSemantics": "Skill guidance headed “Critical Rules”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -386,13 +386,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:595"
|
||||
"skill:skills/paperclip/SKILL.md:598"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:comment-style-required:616",
|
||||
"id": "skill:skills/paperclip/SKILL.md:comment-style-required:619",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:616",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:619",
|
||||
"title": "Comment Style (Required)",
|
||||
"expectedSemantics": "Skill guidance headed “Comment Style (Required)”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -401,13 +401,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:616"
|
||||
"skill:skills/paperclip/SKILL.md:619"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:update:648",
|
||||
"id": "skill:skills/paperclip/SKILL.md:update:651",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:648",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:651",
|
||||
"title": "Update",
|
||||
"expectedSemantics": "Skill guidance headed “Update”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -416,13 +416,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:648"
|
||||
"skill:skills/paperclip/SKILL.md:651"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:task-documents-and-deliverables:658",
|
||||
"id": "skill:skills/paperclip/SKILL.md:task-documents-and-deliverables:661",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:658",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:661",
|
||||
"title": "Task Documents and Deliverables",
|
||||
"expectedSemantics": "Skill guidance headed “Task Documents and Deliverables”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -431,13 +431,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:658"
|
||||
"skill:skills/paperclip/SKILL.md:661"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:672",
|
||||
"id": "skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:675",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:672",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:675",
|
||||
"title": "Planning (Required when planning requested)",
|
||||
"expectedSemantics": "Skill guidance headed “Planning (Required when planning requested)”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -446,13 +446,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:672"
|
||||
"skill:skills/paperclip/SKILL.md:675"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:703",
|
||||
"id": "skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:706",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:703",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:706",
|
||||
"title": "Key Endpoints (Hot Routes)",
|
||||
"expectedSemantics": "Skill guidance headed “Key Endpoints (Hot Routes)”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -461,13 +461,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:703"
|
||||
"skill:skills/paperclip/SKILL.md:706"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:searching-issues:733",
|
||||
"id": "skill:skills/paperclip/SKILL.md:searching-issues:736",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:733",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:736",
|
||||
"title": "Searching Issues",
|
||||
"expectedSemantics": "Skill guidance headed “Searching Issues”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -476,13 +476,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:733"
|
||||
"skill:skills/paperclip/SKILL.md:736"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:full-reference:743",
|
||||
"id": "skill:skills/paperclip/SKILL.md:full-reference:746",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:743",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:746",
|
||||
"title": "Full Reference",
|
||||
"expectedSemantics": "Skill guidance headed “Full Reference”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -491,13 +491,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:743"
|
||||
"skill:skills/paperclip/SKILL.md:746"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:747",
|
||||
"id": "skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:750",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:747",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:750",
|
||||
"title": "Conversational confirmation answers",
|
||||
"expectedSemantics": "Skill guidance headed “Conversational confirmation answers”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -506,13 +506,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:747"
|
||||
"skill:skills/paperclip/SKILL.md:750"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:incidental-feedback:764",
|
||||
"id": "skill:skills/paperclip/SKILL.md:incidental-feedback:767",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:764",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:767",
|
||||
"title": "Incidental feedback",
|
||||
"expectedSemantics": "Skill guidance headed “Incidental feedback”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -521,7 +521,7 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:764"
|
||||
"skill:skills/paperclip/SKILL.md:767"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -87,9 +87,20 @@ vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi
|
||||
|
||||
describe("Copilot build-owned installation", () => {
|
||||
it("admits the current profile with environment launch preparation", () => {
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(52);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(53);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBeUndefined();
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:3614db52d3ddcbc0153c79361a0db2c4324249475d3be490f152b64f916bdd53");
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:6441ff7f3fc664064bff9e7ae80252ddec7a39236f9a4b9d402785eb8b03b2c9");
|
||||
});
|
||||
|
||||
it("preserves v52 while binding the workspace continuation source in v53", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v52-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v53-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:3614db52d3ddcbc0153c79361a0db2c4324249475d3be490f152b64f916bdd53");
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "semanticContinuationSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 52)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v51 when the setup transport contract changes", () => {
|
||||
|
||||
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 52,
|
||||
"agentProfileVersion": 53,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:3614db52d3ddcbc0153c79361a0db2c4324249475d3be490f152b64f916bdd53",
|
||||
"commandDigest": "sha256:6441ff7f3fc664064bff9e7ae80252ddec7a39236f9a4b9d402785eb8b03b2c9",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"claude": {
|
||||
|
||||
@@ -2,13 +2,13 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
|
||||
|
||||
/** Decodable historical profile revisions, not permission to launch them.
|
||||
* Exact current profile/package/digest admission is checked separately. */
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51;
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52;
|
||||
type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"];
|
||||
export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion;
|
||||
|
||||
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
|
||||
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52],
|
||||
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ describe("controller-owned completion continuation", () => {
|
||||
.toEqual({ ...yielded, continuation });
|
||||
});
|
||||
|
||||
it("continues staged workspace preparation from the current server-owned task", async () => {
|
||||
const preparation = { kind: "same_agent", summary: "Continue with the prepared repository.", idempotencyKey: "repository:task:receipt:run" };
|
||||
expect(await resolveCompletionContinuation(yielded, "run", async () => ({ ...context, completionContinuation: preparation })))
|
||||
.toEqual({ ...yielded, continuation: preparation });
|
||||
});
|
||||
|
||||
it.each([
|
||||
null, { ...context, run: { id: "other" } }, { ...context, activeTask: { executionRunId: "other" } },
|
||||
{ ...context, completionContinuation: null }, { ...context, completionContinuation: { kind: "response_wake" } },
|
||||
|
||||
@@ -14,12 +14,12 @@ export async function resolveCompletionContinuation(
|
||||
const task = object(context.activeTask);
|
||||
const continuation = object(context.completionContinuation);
|
||||
if (object(context.run).id !== runId || task.executionRunId !== runId ||
|
||||
continuation.kind !== "monitor" ||
|
||||
!["monitor", "same_agent"].includes(String(continuation.kind)) ||
|
||||
typeof continuation.summary !== "string" || !continuation.summary.trim() ||
|
||||
typeof continuation.idempotencyKey !== "string" || !continuation.idempotencyKey.trim()) {
|
||||
throw new Error("Cannot yield without a continuation: no eligible saved monitor exists on this task. Save a monitor with set_task_monitor, provide an explicit response_wake for a real response wait, or continue the unfinished work. Do not repeat completed actions.");
|
||||
throw new Error("Cannot yield without a continuation: no eligible saved monitor or workspace continuation exists on this task. Save a monitor for a deferred check, provide an explicit response_wake for a real response wait, or continue the unfinished work. Do not repeat completed actions.");
|
||||
}
|
||||
return { ...report, continuation: { kind: "monitor", summary: continuation.summary, idempotencyKey: continuation.idempotencyKey } };
|
||||
return { ...report, continuation: { kind: continuation.kind, summary: continuation.summary, idempotencyKey: continuation.idempotencyKey } };
|
||||
}
|
||||
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"commandDigest": "sha256:6441ff7f3fc664064bff9e7ae80252ddec7a39236f9a4b9d402785eb8b03b2c9",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 53,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
|
||||
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "89e3dde25ff35bfd4d7f520fd9d14af3cb9027f182a2d51431d298f1015be580",
|
||||
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "cc90b59bdfe6a82f7fa3e819e8671f71f7b14047e68942e8086618ffbaacf59f",
|
||||
"semanticDirectDriverSourceSha256": "ff572638f2cb5d9f8249ff8e2f412311c1e3af209ef0bf7e5ea9f10a21d91103",
|
||||
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "f8b381e1ac2a880558f3e81968f0f8f05de3ded573d044fafc89f198f2ec0433",
|
||||
"semanticValidatorsSourceSha256": "f22d9caf2035b600abc1a6625ae23ca97f1409d7e29ed5b7264bb8f9784c7dc9",
|
||||
"semanticSchemaBundleSourceSha256": "016391a087cc217aafed4ba1f4efafc6e4881a7525952dce824b7f4d24530603",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
|
||||
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
|
||||
"configuredEnvironmentSourceSha256": "5c9008d121979a3a8987e6b8a5752172eafdc4a2b5d725a73f3bbb8fb83be8ff",
|
||||
"configuredEnvironmentRustSourceSha256": "85c87fae62b8a9377e4f0a4c00302c7519fae4397b1c48253f8f2b7ba7faf994",
|
||||
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
|
||||
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
|
||||
"profileInstallationSourceSha256": "e4aacb08015fa9104dce2b0c7d150cc432231c8a3e07d2cec2a325b388f7f382",
|
||||
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
|
||||
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
|
||||
"nativeSessionRuntimeSourceSha256": "e8a3bee66a1ff1c6f251b5ae57453ffb908336da9c4a6d98f41f94f7ee94a8e8",
|
||||
"nativeRuntimeContextSourceSha256": "df93bc8bcfa9dfdcf26ad6a69ea0bd3e56ef186c5e962b243fb2288f7074584b",
|
||||
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
|
||||
"harnessSessionContractSourceSha256": "03f4cd7f76f34f6d82e009b66aa51f18f6e23dc235b660778060e35b6ea07412",
|
||||
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
|
||||
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
|
||||
"transportContractSourceSha256": "4fa9593226ff3280bc12140bfcbc2c0986e4cc8d4733381c334138dd03b08f46",
|
||||
"nativeBackendSourceSha256": "2406483fd35a564971c22e10fc2bad26bae16ff5a3fb6c65690fcf156522ab6d",
|
||||
"nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44",
|
||||
"nativeInstructionsSourceSha256": "ee8df5d85b263d5d2d0dc9ede8602cb7daf6388687108fe05e2371f5f56c3492",
|
||||
"providerLaunchSourceSha256": "cba71368f82ae8d300123efa09f23744e19e82142788fa9ad238338fefec9624",
|
||||
"launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296",
|
||||
"runtimeRequestBridgeSourceSha256": "ec09328c11bb6688269b251a40f84e6c810f32afcb0f31cb79a26ca48be68e73",
|
||||
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
|
||||
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
|
||||
"guardedInstallationSourceSha256": "f6d0c33de38259efefa0f4e0f9737cd30cfd3996a8e875e3fbdb8c72dc133dca",
|
||||
"nativeExecutionSourceSha256": "fc7c6568feb0d8d709e9baa51839420c9b90336b4865a9b8405aba06e4d1e9c4",
|
||||
"profileCompatibilitySourceSha256": "48f589d87ef1459d02134c03c768457fa35123b685573ae29f49d73ff593d817",
|
||||
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
|
||||
"assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c",
|
||||
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
|
||||
"credentialBindingSourceSha256": "54fd09ad430dac04df98d829061f6701e73b5bf2a0f4de836d2ac36ee511009f",
|
||||
"sidecarErrorSourceSha256": "b09af4577f8bf42eee7036b2ad9842202cfc763e72b53c3287f82c6090cdad80",
|
||||
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
|
||||
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
|
||||
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticContinuationSourceSha256": "1154778050a39178b09e9fe835173592691f6b679348814f30eaae509d84db0e",
|
||||
"runtimeHostSourceSha256": "60e5454f030e87f2afa8b1fb120b26adb126b6b17d0e5e82f49c77f76f466cbb",
|
||||
"runtimeAdapterSourceSha256": "e4c764c2c3c7dd655a965b39472bd1ad8d577d1e7258b25594b171c9429752b8",
|
||||
"runtimeSandboxSourceSha256": "92dfa53f5a3ed0062913ae417eeccec243b430d444495efde2a8e9c4c02e78ea",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
projectHistoricalHeartbeatRunComment,
|
||||
findHeartbeatRunCompletionComment,
|
||||
isExternalChatPresentationContext,
|
||||
isChatOriginPublicationCandidate,
|
||||
mergeHeartbeatRunResultJson,
|
||||
readCompletedAssistantMessageCandidate,
|
||||
resolveHeartbeatRunResponse,
|
||||
@@ -61,6 +62,19 @@ describe("scheduled continuation presentation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("continuation origin verification", () => {
|
||||
it.each(["native_status_decision", "issue.monitor", "tool_action_review", "issue.comment", "issue.comment.created", "issue.update"])
|
||||
("checks %s without treating candidacy as publication authority", source => {
|
||||
expect(isChatOriginPublicationCandidate({ source })).toBe(true);
|
||||
expect(isExternalChatPresentationContext({ source })).toBe(false);
|
||||
expect(isExternalChatPresentationContext({ source }, true)).toBe(true);
|
||||
});
|
||||
it("keeps internal work internal", () => {
|
||||
expect(isChatOriginPublicationCandidate({ source: "issue.assigned" })).toBe(false);
|
||||
expect(isExternalChatPresentationContext({ source: "issue.assigned" }, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectHeartbeatRunFinalAgentMessage", () => {
|
||||
const substantive = {
|
||||
seq: 80,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { heartbeatRuns } from "@paperclipai/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { vi, type Mock, type Mocked } from "vitest";
|
||||
import { createRunDispatch } from "../../modules/run-dispatch/index.js";
|
||||
import type { createWakeQueue } from "../../modules/wake-queue/index.js";
|
||||
import { createHeartbeatRunState } from "../../services/heartbeat/run-state.js";
|
||||
import { createHeartbeatRunPreparation } from "../../services/heartbeat/run-preparation.js";
|
||||
import type { HeartbeatQueueDependencies } from "../../services/heartbeat/queue.js";
|
||||
|
||||
type Run = typeof heartbeatRuns.$inferSelect;
|
||||
type MockedCallback = "getAgent" | "setRunStatus" | "setWakeupStatus" | "appendRunEvent" | "finalizeAgentStatus" | "getAgentInvokability" | "getWorktreeExecutionCutoff" | "applyRunDispatchPostCommitEffects" | "getSchedulingSuppression" | "canStartNewExternalBotWork" | "executeRun" | "applyWakeQueuePostCommitEffects" | "releaseIssueExecutionAndPromote" | "publishRunLifecyclePluginEvent" | "cancelRunInternal" | "cancelActiveForAgentInternal" | "resumeExecutionWaitComments" | "resumeQueuedCommentInterrupt" | "resumeSavedLegacyComments" | "sweepPendingCleanupLeases";
|
||||
type QueueTestDependencies = HeartbeatQueueDependencies & { [K in MockedCallback]: Mock<HeartbeatQueueDependencies[K]> } & {
|
||||
issuesSvc: Mocked<HeartbeatQueueDependencies["issuesSvc"]>;
|
||||
treeControlSvc: Mocked<HeartbeatQueueDependencies["treeControlSvc"]>;
|
||||
budgets: Mocked<HeartbeatQueueDependencies["budgets"]>;
|
||||
instanceSettings: Mocked<HeartbeatQueueDependencies["instanceSettings"]>;
|
||||
wakeQueue: Mocked<HeartbeatQueueDependencies["wakeQueue"]>;
|
||||
liveRunExecutions: Mocked<HeartbeatQueueDependencies["liveRunExecutions"]>;
|
||||
};
|
||||
|
||||
export function createHeartbeatQueueTestDependencies(db: Db): QueueTestDependencies {
|
||||
const state = createHeartbeatRunState(db);
|
||||
const preparation = createHeartbeatRunPreparation(db);
|
||||
return {
|
||||
...preparation,
|
||||
...state,
|
||||
getAgent: vi.fn<HeartbeatQueueDependencies["getAgent"]>(async () => { throw new Error("Unexpected agent lookup"); }),
|
||||
options: {},
|
||||
setRunStatus: vi.fn<HeartbeatQueueDependencies["setRunStatus"]>(async (id, status, patch) =>
|
||||
db.update(heartbeatRuns).set({ ...patch, status }).where(eq(heartbeatRuns.id, id)).returning().then(rows => rows[0] ?? null)),
|
||||
setWakeupStatus: vi.fn<HeartbeatQueueDependencies["setWakeupStatus"]>(async () => undefined),
|
||||
appendRunEvent: vi.fn<HeartbeatQueueDependencies["appendRunEvent"]>(async () => undefined),
|
||||
finalizeAgentStatus: vi.fn<HeartbeatQueueDependencies["finalizeAgentStatus"]>(async () => undefined),
|
||||
getAgentInvokability: vi.fn<HeartbeatQueueDependencies["getAgentInvokability"]>(async () => ({ invokable: true })),
|
||||
getWorktreeExecutionCutoff: vi.fn<HeartbeatQueueDependencies["getWorktreeExecutionCutoff"]>(async () => null),
|
||||
applyRunDispatchPostCommitEffects: vi.fn<HeartbeatQueueDependencies["applyRunDispatchPostCommitEffects"]>(),
|
||||
issuesSvc: {
|
||||
getDependencyReadiness: vi.fn<HeartbeatQueueDependencies["issuesSvc"]["getDependencyReadiness"]>(),
|
||||
listDependencyReadiness: vi.fn(async () => new Map()),
|
||||
},
|
||||
treeControlSvc: { getActivePauseHoldGate: vi.fn(async () => null) },
|
||||
budgets: { getInvocationBlock: vi.fn(async () => null) },
|
||||
instanceSettings: { getExperimental: vi.fn<HeartbeatQueueDependencies["instanceSettings"]["getExperimental"]>() },
|
||||
runDispatch: createRunDispatch(db),
|
||||
wakeQueue: {
|
||||
createAdmissionTransactionScope: vi.fn<ReturnType<typeof createWakeQueue>["createAdmissionTransactionScope"]>(),
|
||||
admitWakeBehindIssueExecution: vi.fn<ReturnType<typeof createWakeQueue>["admitWakeBehindIssueExecution"]>(),
|
||||
},
|
||||
activeRunExecutions: new Set<string>(),
|
||||
activeRunExecutionPromises: new Set<Promise<void>>(),
|
||||
activeWakeupPromises: new Set<Promise<unknown>>(),
|
||||
liveRunExecutions: { has: vi.fn(() => false) },
|
||||
isHeartbeatRunTerminalStatus: (status: string | null | undefined) => ["succeeded", "failed", "cancelled", "timed_out"].includes(status ?? ""),
|
||||
isSameTaskScope: (left: string | null, right: string | null) => left === right,
|
||||
runTaskKey: (run: Run) => typeof run.contextSnapshot?.issueId === "string" ? run.contextSnapshot.issueId : null,
|
||||
filterZombieCoalesceTarget: <T extends { id: string; status: string }>(run: T | null) => run,
|
||||
getSchedulingSuppression: vi.fn<HeartbeatQueueDependencies["getSchedulingSuppression"]>(async () => ({ suppressed: false, reason: null } as Awaited<ReturnType<HeartbeatQueueDependencies["getSchedulingSuppression"]>>)),
|
||||
canStartNewExternalBotWork: vi.fn<HeartbeatQueueDependencies["canStartNewExternalBotWork"]>(async () => true),
|
||||
executeRun: vi.fn<HeartbeatQueueDependencies["executeRun"]>(async () => undefined),
|
||||
applyWakeQueuePostCommitEffects: vi.fn<HeartbeatQueueDependencies["applyWakeQueuePostCommitEffects"]>(async () => undefined),
|
||||
releaseIssueExecutionAndPromote: vi.fn<HeartbeatQueueDependencies["releaseIssueExecutionAndPromote"]>(async () => undefined),
|
||||
publishRunLifecyclePluginEvent: vi.fn<HeartbeatQueueDependencies["publishRunLifecyclePluginEvent"]>(),
|
||||
cancelRunInternal: vi.fn<HeartbeatQueueDependencies["cancelRunInternal"]>(async () => undefined),
|
||||
cancelActiveForAgentInternal: vi.fn<HeartbeatQueueDependencies["cancelActiveForAgentInternal"]>(async () => undefined),
|
||||
resumeExecutionWaitComments: vi.fn<HeartbeatQueueDependencies["resumeExecutionWaitComments"]>(async () => undefined),
|
||||
resumeQueuedCommentInterrupt: vi.fn<HeartbeatQueueDependencies["resumeQueuedCommentInterrupt"]>(async () => undefined),
|
||||
resumeSavedLegacyComments: vi.fn<HeartbeatQueueDependencies["resumeSavedLegacyComments"]>(async () => undefined),
|
||||
formatIssueIdentifierLink: (_identifier: string | null, fallback: string) => fallback,
|
||||
sweepPendingCleanupLeases: vi.fn<HeartbeatQueueDependencies["sweepPendingCleanupLeases"]>(),
|
||||
} satisfies HeartbeatQueueDependencies;
|
||||
}
|
||||
@@ -51,6 +51,13 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
const first = await call("prepare_repository", args) as { preparationId: string; kind: string };
|
||||
expect(first.kind).toBe("requires_next_admission");
|
||||
expect(await call("prepare_repository", args)).toMatchObject({ preparationId: first.preparationId });
|
||||
const continued = await call("get_task_context", {}) as { completionContinuation: { kind: string; idempotencyKey: string } };
|
||||
expect(continued.completionContinuation).toMatchObject({ kind: "same_agent" });
|
||||
expect((await call("get_task_context", {}) as typeof continued).completionContinuation).toEqual(continued.completionContinuation);
|
||||
await server.db.update(executionWorkspaceRepositories).set({ requestedByIssueId: f.blockerId }).where(eq(executionWorkspaceRepositories.id, first.preparationId));
|
||||
expect(await call("get_task_context", {})).toMatchObject({ completionContinuation: null });
|
||||
await server.db.update(executionWorkspaceRepositories).set({ requestedByIssueId: f.issueId, state: "ready" }).where(eq(executionWorkspaceRepositories.id, first.preparationId));
|
||||
expect(await call("get_task_context", {})).toMatchObject({ completionContinuation: null });
|
||||
expect(await call("select_workspace", { selection: { kind: "task_directory" }, expectedBindingRevision: view.bindingRevision, requestKey: "root-one" })).toMatchObject({ kind: "scheduled", applies: "next_normal_admission" });
|
||||
const [task] = await server.db.select().from(issues).where(eq(issues.id, f.issueId));
|
||||
expect(task).toMatchObject({ executionWorkspaceId: workspace.id, projectId: null, workspacePendingSelection: { requestKey: "root-one" } });
|
||||
|
||||
@@ -3,15 +3,18 @@ import { and, eq } from "drizzle-orm";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
agents, agentWakeupRequests, authUsers, chatConversations, chatEndpoints, chatMessageLinks,
|
||||
chatPublications, companies, completionContracts, createDb, heartbeatRuns, issueComments, issues, nativeRunFinalizations, nativeRunResults,
|
||||
toolApplications, toolConnections,
|
||||
chatPublications, companies, companyMemberships, completionContracts, createDb, heartbeatRuns, issueComments, issues, nativeRunFinalizations, nativeRunResults,
|
||||
statusDecisions, statusDecisionEffects, toolApplications, toolConnections,
|
||||
} from "@paperclipai/db";
|
||||
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "../__tests__/helpers/embedded-postgres.js";
|
||||
import { enqueueChatRunMilestones, resolveChatRunPresentationAuthorizationReason } from "./chat-run-publications.js";
|
||||
import { CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON } from "./heartbeat-run-summary.js";
|
||||
import { CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON, isChatOriginPublicationCandidate, isExternalChatPresentationContext, resolveHeartbeatRunResponse } from "./heartbeat-run-summary.js";
|
||||
import { resolveChatOriginPublicationBindings } from "./issues.js";
|
||||
import { NativeRunCoordinatorStore } from "./native-runtime/native-run-coordinator-store.js";
|
||||
import { finalizeNativeRun, repairCommittedNativeChatResponse } from "./native-runtime/native-run-finalizer.js";
|
||||
import { createHeartbeatQueueTestDependencies } from "../__tests__/helpers/heartbeat-queue.js";
|
||||
import { createHeartbeatQueue } from "./heartbeat/queue.js";
|
||||
import { instanceSettingsService } from "./instance-settings.js";
|
||||
|
||||
const support = await getEmbeddedPostgresTestSupport();
|
||||
if (!support.supported) console.warn(`Monitor publication database unavailable: ${support.reason}`);
|
||||
@@ -27,8 +30,9 @@ if (!support.supported) console.warn(`Monitor publication database unavailable:
|
||||
|
||||
async function fixture() {
|
||||
const [company] = await db.insert(companies).values({ name: "Continuation", issuePrefix: randomUUID().slice(0, 8) }).returning();
|
||||
await db.insert(companyMemberships).values({ companyId: company.id, principalType: "user", principalId: "requester", status: "active", membershipRole: "owner" });
|
||||
const [agent] = await db.insert(agents).values({ companyId: company.id, name: "Worker", adapterType: "paperclip_runner" }).returning();
|
||||
const [issue] = await db.insert(issues).values({ companyId: company.id, title: "Repository QA", status: "in_progress", assigneeAgentId: agent.id }).returning();
|
||||
const [issue] = await db.insert(issues).values({ companyId: company.id, title: "Repository QA", status: "in_progress", assigneeAgentId: agent.id, responsibleUserId: "requester" }).returning();
|
||||
const [application] = await db.insert(toolApplications).values({ companyId: company.id, applicationKey: randomUUID(), name: "Slack", type: "chat" }).returning();
|
||||
const [connection] = await db.insert(toolConnections).values({ companyId: company.id, applicationId: application.id, name: "Slack", uid: randomUUID(), connectionPurpose: "channel", transport: "chat_sdk" }).returning();
|
||||
const [endpoint] = await db.insert(chatEndpoints).values({ companyId: company.id, connectionId: connection.id, provider: "slack", publicId: randomUUID(), assignedAgentId: agent.id, status: "active" }).returning();
|
||||
@@ -133,6 +137,95 @@ if (!support.supported) console.warn(`Monitor publication database unavailable:
|
||||
expect(await db.select().from(chatPublications).where(and(eq(chatPublications.issueId, f.issue.id), eq(chatPublications.idempotencyKey, `run:${f.source.id}:completed:${f.endpoint.id}`)))).toEqual([]);
|
||||
});
|
||||
|
||||
async function continuedTaskFixture() {
|
||||
const f = await fixture();
|
||||
const [contract] = await db.insert(completionContracts).values({ companyId: f.company.id, issueId: f.issue.id,
|
||||
revision: 1, schemaVersion: "paperclip.completion-contract.v1", policyVersion: "test", risk: "standard",
|
||||
completionAuthority: "server_arbiter", incompleteCriteriaPolicy: "preserve_non_terminal",
|
||||
contractJson: { revision: "test", objective: "Verify repository migration", criteria: [] }, canonicalSha256: "workspace-contract",
|
||||
createdByActorType: "system", createdByActorId: "test",
|
||||
}).returning();
|
||||
const sessionId = randomUUID(), runnerId = randomUUID();
|
||||
await db.update(heartbeatRuns).set({ status: "running", nativeSessionId: sessionId, runnerInstanceId: runnerId,
|
||||
completionContractId: contract.id, completionContractSha256: contract.canonicalSha256,
|
||||
}).where(eq(heartbeatRuns.id, f.source.id));
|
||||
await db.update(issues).set({ executionRunId: f.source.id }).where(eq(issues.id, f.issue.id));
|
||||
await db.update(heartbeatRuns).set({ status: "cancelled", finishedAt: new Date() }).where(eq(heartbeatRuns.id, f.successor.id));
|
||||
const port = new NativeRunCoordinatorStore(db, { companyId: f.company.id, issueId: f.issue.id,
|
||||
runId: f.source.id, agentId: f.agent.id, normalizedSessionId: sessionId, completionContractId: contract.id,
|
||||
completionContractSha256: contract.canonicalSha256, runnerSourceInstanceId: runnerId,
|
||||
completionContractRevision: "test", completionContractCriterionIds: [],
|
||||
});
|
||||
const summary = "The repository is staged. I will continue QA automatically when it is prepared.";
|
||||
await port.completeRun({ result: { schema: "paperclip.run_result.v1", reportedWorkDisposition: "yielded", summary,
|
||||
completionClaim: { contractRevision: "test", objectiveSatisfied: false, criteria: [], remainingWork: [{ description: "Complete QA", blocksCompletion: true }] },
|
||||
continuation: { kind: "same_agent", summary: "Continue QA with the prepared checkout", idempotencyKey: "repository-admission" },
|
||||
evidence: [], verification: [], attentionRequests: [], artifacts: [],
|
||||
}, terminal: { schema: "paperclip.prp.terminal.v1", runTerminalState: "succeeded", turnTerminalState: "completed", reportedWorkDisposition: "yielded" }, turnId: "prepare-turn" });
|
||||
await finalizeNativeRun({ db, runId: f.source.id, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
|
||||
const [wake] = await db.select().from(agentWakeupRequests).where(and(eq(agentWakeupRequests.companyId, f.company.id), eq(agentWakeupRequests.reason, "issue_status_changed")));
|
||||
expect(wake).toBeDefined();
|
||||
const deps = createHeartbeatQueueTestDependencies(db);
|
||||
deps.getAgent.mockImplementation(async id => (await db.select().from(agents).where(eq(agents.id, id)))[0] ?? null);
|
||||
deps.instanceSettings.getExperimental.mockImplementation(() => instanceSettingsService(db).getExperimental());
|
||||
await db.update(agents).set({ runtimeConfig: { heartbeat: { wakeOnDemand: true } } }).where(eq(agents.id, f.agent.id));
|
||||
const queue = createHeartbeatQueue(db, deps);
|
||||
expect(await queue.dispatchPendingNativeStatusWakeups({ companyId: f.company.id })).toMatchObject({ dispatched: 1 });
|
||||
await queue.startNextQueuedRunForAgent(f.agent.id);
|
||||
await Promise.all([...deps.activeRunExecutionPromises]);
|
||||
const [delivered] = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, wake.id));
|
||||
expect(delivered).toMatchObject({ status: "coalesced" });
|
||||
expect(delivered.runId).toBeTruthy();
|
||||
const [successor] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, delivered.runId!));
|
||||
expect(successor.responsibleUserId).toBe("requester");
|
||||
return { ...f, successor, continuationWake: delivered, summary,
|
||||
bindings: (runId = successor.id) => resolveChatOriginPublicationBindings(db, f.company.id, f.issue.id, runId) };
|
||||
}
|
||||
|
||||
it("delivers the saved setup explanation and retains chat through the committed automatic continuation", async () => {
|
||||
const f = await continuedTaskFixture();
|
||||
expect(await db.select({ body: issueComments.body }).from(issueComments).where(eq(issueComments.createdByRunId, f.source.id)))
|
||||
.toEqual([{ body: f.summary }]);
|
||||
expect(await f.bindings()).toEqual(await f.bindings(f.source.id));
|
||||
expect(await f.bindings()).toHaveLength(1);
|
||||
await enqueueChatRunMilestones(db);
|
||||
const publications = await db.select().from(chatPublications).where(eq(chatPublications.issueId, f.issue.id));
|
||||
expect(publications.some(p => p.payload.text === f.summary && p.conversationId === f.conversation.id)).toBe(true);
|
||||
expect(publications.some(p => p.payload.progressState === "working" && p.conversationId === f.conversation.id)).toBe(true);
|
||||
expect(await repairCommittedNativeChatResponse(db, { companyId: f.company.id, issueId: f.issue.id, runId: f.source.id })).toBe(false);
|
||||
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.source.id))).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("selects the continued chat run's completed answer over an earlier bookkeeping comment", async () => {
|
||||
const f = await continuedTaskFixture();
|
||||
const authorization = isChatOriginPublicationCandidate(f.successor.contextSnapshot)
|
||||
? await resolveChatRunPresentationAuthorizationReason(db, { companyId: f.company.id, issueId: f.issue.id, runId: f.successor.id }) : null;
|
||||
const context = isExternalChatPresentationContext(f.successor.contextSnapshot, authorization === CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON);
|
||||
const answer = "Migration QA is complete. The verified report is available at the attached result.";
|
||||
const resolved = resolveHeartbeatRunResponse({ resultJson: {},
|
||||
existingComment: { id: "earlier-comment", body: "The checkout is ready; QA is underway." },
|
||||
finalAgentMessage: { text: answer, channel: "final", sourceEventId: "completed-final" },
|
||||
preferFinalResponseOverExistingComment: context,
|
||||
});
|
||||
expect(resolved.text).toBe(answer);
|
||||
expect(resolved.decision).toMatchObject({ chosenSource: "final_agent_message", commentAction: "create" });
|
||||
});
|
||||
|
||||
it.each(["effect", "decision", "wake", "dispatch", "requester", "agent"])("rejects automatic continuation when %s authority is no longer valid", async fault => {
|
||||
const f = await continuedTaskFixture();
|
||||
if (fault === "effect") await db.update(statusDecisionEffects).set({ targetId: randomUUID() }).where(eq(statusDecisionEffects.companyId, f.company.id));
|
||||
if (fault === "decision") await db.update(statusDecisions).set({ applicationState: "proposed" }).where(eq(statusDecisions.companyId, f.company.id));
|
||||
if (fault === "wake") await db.update(agentWakeupRequests).set({ idempotencyKey: randomUUID() }).where(eq(agentWakeupRequests.id, f.continuationWake.id));
|
||||
if (fault === "dispatch") await db.update(agentWakeupRequests).set({ requestedByActorId: "unrelated-dispatch" }).where(and(
|
||||
eq(agentWakeupRequests.companyId, f.company.id), eq(agentWakeupRequests.requestedByActorId, `native-status-wake-dispatch:${f.continuationWake.id}`)));
|
||||
if (fault === "requester") await db.update(heartbeatRuns).set({ responsibleUserId: "another-person" }).where(eq(heartbeatRuns.id, f.successor.id));
|
||||
if (fault === "agent") {
|
||||
const [other] = await db.insert(agents).values({ companyId: f.company.id, name: "Other", adapterType: "paperclip_runner" }).returning();
|
||||
await db.update(heartbeatRuns).set({ agentId: other.id }).where(eq(heartbeatRuns.id, f.source.id));
|
||||
}
|
||||
expect(await f.bindings()).toEqual([]);
|
||||
});
|
||||
|
||||
it("recovers the accepted continuation explanation after interrupted finalization without another provider result", async () => {
|
||||
const f = await fixture();
|
||||
const [contract] = await db.insert(completionContracts).values({ companyId: f.company.id, issueId: f.issue.id,
|
||||
|
||||
@@ -748,6 +748,19 @@ export async function enqueueChatRunMilestones(
|
||||
and monitor_wake.payload->>'issueId' = ${issueIdFromContext}
|
||||
)
|
||||
)`;
|
||||
// This is only a candidate prefilter. The origin resolver below requires the
|
||||
// committed decision/effect ledger before selecting any conversation.
|
||||
const hasTaskContinuation = sql<boolean>`(
|
||||
${heartbeatRuns.contextSnapshot}->>'source' = 'native_status_decision' and exists (
|
||||
select 1 from agent_wakeup_requests continuation_wake
|
||||
where continuation_wake.company_id = ${heartbeatRuns.companyId}
|
||||
and continuation_wake.run_id = ${heartbeatRuns.id}
|
||||
and continuation_wake.agent_id = ${heartbeatRuns.agentId}
|
||||
and continuation_wake.reason = 'issue_status_changed'
|
||||
and continuation_wake.status in ('queued', 'claimed', 'completed', 'coalesced')
|
||||
and continuation_wake.payload->>'issueId' = ${issueIdFromContext}
|
||||
)
|
||||
)`;
|
||||
let inserted = 0;
|
||||
let cursor: {
|
||||
updatedAt: Date;
|
||||
@@ -835,6 +848,7 @@ export async function enqueueChatRunMilestones(
|
||||
),
|
||||
hasQuestionContinuationTarget,
|
||||
hasMonitorContinuation,
|
||||
hasTaskContinuation,
|
||||
),
|
||||
// Heartbeat marks a run succeeded before the presentation resolver
|
||||
// finishes. Waiting for its durable decision prevents a generic
|
||||
@@ -884,6 +898,7 @@ export async function enqueueChatRunMilestones(
|
||||
),
|
||||
or(
|
||||
hasMonitorContinuation,
|
||||
hasTaskContinuation,
|
||||
sql`exists (
|
||||
select 1
|
||||
from ${chatMessageLinks}
|
||||
|
||||
@@ -192,6 +192,11 @@ function record(value: unknown): Record<string, unknown> {
|
||||
: {};
|
||||
}
|
||||
|
||||
function requiresChatOriginProof(source: string): boolean {
|
||||
return source === "tool_action_review" || source === "issue.monitor" || source === "native_status_decision" ||
|
||||
source.startsWith("issue.comment") || source === "issue.update";
|
||||
}
|
||||
|
||||
export function isExternalChatPresentationContext(
|
||||
contextSnapshot: unknown,
|
||||
verifiedToolReviewChatOrigin = false,
|
||||
@@ -202,12 +207,7 @@ export function isExternalChatPresentationContext(
|
||||
typeof context.source === "string" ? context.source.trim() : "";
|
||||
// Approvals and Board comments also resume ordinary internal tasks. Only
|
||||
// durable source-run or mirrored-comment proof authorizes an external reply.
|
||||
if (
|
||||
source === "tool_action_review" ||
|
||||
source === "issue.monitor" ||
|
||||
source.startsWith("issue.comment") ||
|
||||
source === "issue.update"
|
||||
)
|
||||
if (requiresChatOriginProof(source))
|
||||
return verifiedToolReviewChatOrigin;
|
||||
return (
|
||||
source.startsWith("chat:") ||
|
||||
@@ -216,6 +216,12 @@ export function isExternalChatPresentationContext(
|
||||
);
|
||||
}
|
||||
|
||||
export function isChatOriginPublicationCandidate(contextSnapshot: unknown): boolean {
|
||||
const context = record(contextSnapshot);
|
||||
const source = typeof context.source === "string" ? context.source.trim() : "";
|
||||
return requiresChatOriginProof(source) || isExternalChatPresentationContext(contextSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read only completed assistant prose that can participate in final-response
|
||||
* presentation. A canonical `final` is authoritative; `unknown` is retained
|
||||
@@ -285,16 +291,18 @@ function hasYieldedSemanticResult(resultJson: Record<string, unknown>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function readAcceptedMonitorContinuationSummary(
|
||||
export function readAcceptedTaskContinuationSummary(
|
||||
resultJson: Record<string, unknown> | null | undefined,
|
||||
): string | null {
|
||||
const result = record(resultJson);
|
||||
if (result.finalizationPhase !== "committed" ||
|
||||
!["scheduled_monitor_waiting", "scheduled_monitor_dispatched"].includes(String(result.finalizationReasonCode))) return null;
|
||||
if (result.finalizationPhase !== "committed") return null;
|
||||
const kind = result.finalizationReasonCode === "live_continuation_registered" ? "same_agent"
|
||||
: ["scheduled_monitor_waiting", "scheduled_monitor_dispatched"].includes(String(result.finalizationReasonCode)) ? "monitor" : null;
|
||||
if (!kind) return null;
|
||||
for (const candidate of semanticResultCandidates(result)) {
|
||||
const continuation = record(candidate.continuation);
|
||||
if (candidate.schema === "paperclip.run_result.v1" && candidate.reportedWorkDisposition === "yielded" &&
|
||||
continuation.kind === "monitor" && readCommentText(continuation.idempotencyKey)) {
|
||||
continuation.kind === kind && readCommentText(continuation.idempotencyKey)) {
|
||||
return readCommentText(candidate.summary);
|
||||
}
|
||||
}
|
||||
@@ -411,20 +419,21 @@ export function resolveHeartbeatRunResponse(input: {
|
||||
} | null;
|
||||
}): ResolvedHeartbeatRunResponse {
|
||||
const resultJson = record(input.resultJson);
|
||||
const monitorSummary = readAcceptedMonitorContinuationSummary(resultJson);
|
||||
if (monitorSummary) {
|
||||
const continuationSummary = readAcceptedTaskContinuationSummary(resultJson);
|
||||
if (continuationSummary) {
|
||||
if (input.interactionOwnsResponse) return {
|
||||
text: null,
|
||||
decision: decision("none", { commentAction: "none", reasonCodes: ["interaction_owns_response"] }),
|
||||
};
|
||||
const existing = input.existingComment;
|
||||
const reuse = existing?.body === monitorSummary;
|
||||
const reuse = existing?.body === continuationSummary;
|
||||
return {
|
||||
text: monitorSummary,
|
||||
text: continuationSummary,
|
||||
decision: decision("semantic_result_summary", {
|
||||
commentAction: reuse ? "reuse" : "create",
|
||||
...(reuse ? { commentId: existing.id } : {}),
|
||||
reasonCodes: ["accepted_scheduled_monitor_summary"],
|
||||
reasonCodes: [resultJson.finalizationReasonCode === "live_continuation_registered"
|
||||
? "accepted_task_continuation_summary" : "accepted_scheduled_monitor_summary"],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,66 +4,12 @@ import { agents, agentWakeupRequests, caseHandoffs, cases, companies, createDb,
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
|
||||
import { createRunDispatch } from "../../modules/run-dispatch/index.js";
|
||||
import type { createWakeQueue } from "../../modules/wake-queue/index.js";
|
||||
import { createPostgresWakeQueueAdapter } from "../../modules/wake-queue/adapters/postgres.js";
|
||||
import { createReleaseIssueExecution } from "../../modules/wake-queue/application/use-cases.js";
|
||||
import { createHeartbeatRunState } from "./run-state.js";
|
||||
import { createHeartbeatRunPreparation } from "./run-preparation.js";
|
||||
import { createHeartbeatQueue, type HeartbeatQueueDependencies } from "./queue.js";
|
||||
|
||||
type Run = typeof heartbeatRuns.$inferSelect;
|
||||
function callbacks(db: Db) {
|
||||
const state = createHeartbeatRunState(db);
|
||||
const preparation = createHeartbeatRunPreparation(db);
|
||||
return {
|
||||
...preparation,
|
||||
...state,
|
||||
getAgent: vi.fn<HeartbeatQueueDependencies["getAgent"]>(async () => { throw new Error("Unexpected agent lookup"); }),
|
||||
options: {},
|
||||
setRunStatus: vi.fn<HeartbeatQueueDependencies["setRunStatus"]>(async (id, status, patch) =>
|
||||
db.update(heartbeatRuns).set({ ...patch, status }).where(eq(heartbeatRuns.id, id)).returning().then(rows => rows[0] ?? null)),
|
||||
setWakeupStatus: vi.fn<HeartbeatQueueDependencies["setWakeupStatus"]>(async () => undefined),
|
||||
appendRunEvent: vi.fn<HeartbeatQueueDependencies["appendRunEvent"]>(async () => undefined),
|
||||
finalizeAgentStatus: vi.fn(async () => undefined),
|
||||
getAgentInvokability: vi.fn<HeartbeatQueueDependencies["getAgentInvokability"]>(async () => ({ invokable: true })),
|
||||
getWorktreeExecutionCutoff: vi.fn(async () => null),
|
||||
applyRunDispatchPostCommitEffects: vi.fn(),
|
||||
issuesSvc: {
|
||||
getDependencyReadiness: vi.fn<HeartbeatQueueDependencies["issuesSvc"]["getDependencyReadiness"]>(),
|
||||
listDependencyReadiness: vi.fn(async () => new Map()),
|
||||
},
|
||||
treeControlSvc: { getActivePauseHoldGate: vi.fn(async () => null) },
|
||||
budgets: { getInvocationBlock: vi.fn(async () => null) },
|
||||
instanceSettings: { getExperimental: vi.fn<HeartbeatQueueDependencies["instanceSettings"]["getExperimental"]>() },
|
||||
runDispatch: createRunDispatch(db),
|
||||
wakeQueue: {
|
||||
createAdmissionTransactionScope: vi.fn<ReturnType<typeof createWakeQueue>["createAdmissionTransactionScope"]>(),
|
||||
admitWakeBehindIssueExecution: vi.fn<ReturnType<typeof createWakeQueue>["admitWakeBehindIssueExecution"]>(),
|
||||
},
|
||||
activeRunExecutions: new Set<string>(),
|
||||
activeRunExecutionPromises: new Set<Promise<void>>(),
|
||||
activeWakeupPromises: new Set<Promise<unknown>>(),
|
||||
liveRunExecutions: { has: vi.fn(() => false) },
|
||||
isHeartbeatRunTerminalStatus: (status: string | null | undefined) => ["succeeded", "failed", "cancelled", "timed_out"].includes(status ?? ""),
|
||||
isSameTaskScope: (left: string | null, right: string | null) => left === right,
|
||||
runTaskKey: (run: Run) => typeof run.contextSnapshot?.issueId === "string" ? run.contextSnapshot.issueId : null,
|
||||
filterZombieCoalesceTarget: <T extends { id: string; status: string }>(run: T | null) => run,
|
||||
getSchedulingSuppression: vi.fn(async () => ({ suppressed: false, reason: null } as Awaited<ReturnType<HeartbeatQueueDependencies["getSchedulingSuppression"]>>)),
|
||||
canStartNewExternalBotWork: vi.fn(async () => true),
|
||||
executeRun: vi.fn(async () => undefined),
|
||||
applyWakeQueuePostCommitEffects: vi.fn(async () => undefined),
|
||||
releaseIssueExecutionAndPromote: vi.fn<HeartbeatQueueDependencies["releaseIssueExecutionAndPromote"]>(async () => undefined),
|
||||
publishRunLifecyclePluginEvent: vi.fn(),
|
||||
cancelRunInternal: vi.fn(async () => undefined),
|
||||
cancelActiveForAgentInternal: vi.fn(async () => undefined),
|
||||
resumeExecutionWaitComments: vi.fn(async () => undefined),
|
||||
resumeQueuedCommentInterrupt: vi.fn(async () => undefined),
|
||||
resumeSavedLegacyComments: vi.fn(async () => undefined),
|
||||
formatIssueIdentifierLink: (_identifier: string | null, fallback: string) => fallback,
|
||||
sweepPendingCleanupLeases: vi.fn<HeartbeatQueueDependencies["sweepPendingCleanupLeases"]>(),
|
||||
} satisfies HeartbeatQueueDependencies;
|
||||
}
|
||||
import { createHeartbeatQueueTestDependencies as callbacks } from "../../__tests__/helpers/heartbeat-queue.js";
|
||||
|
||||
function guardedDatabase() {
|
||||
const access = vi.fn(() => { throw new Error("Unexpected queue database access"); });
|
||||
return { db: new Proxy({}, { get: access }) as Db, access };
|
||||
|
||||
@@ -20,7 +20,7 @@ import { HttpError } from "../../errors.js";
|
||||
import { logger } from "../../middleware/logger.js";
|
||||
import { normalizeResponsibleUserDenialCode } from "../responsible-user-denial-run-outcomes.js";
|
||||
import { parseObject } from "../../adapters/utils.js";
|
||||
import { CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON, isExternalChatPresentationContext, mergeHeartbeatRunResultJson, resolveHeartbeatRunResponse, type RunPresentationDecision } from "../heartbeat-run-summary.js";
|
||||
import { CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON, isChatOriginPublicationCandidate, isExternalChatPresentationContext, mergeHeartbeatRunResultJson, resolveHeartbeatRunResponse, type RunPresentationDecision } from "../heartbeat-run-summary.js";
|
||||
import { normalizeMaxTurnStopReason } from "../heartbeat-stop-metadata.js";
|
||||
import { CHAT_CONTROL_RECOVERY_UNRESOLVED_CODE } from "../chat-control-recovery-stop.js";
|
||||
import { logActivity } from "../activity-log.js";
|
||||
@@ -28,7 +28,7 @@ import { isUnresolvedWorkspaceBaseRefError, readUnresolvedWorkspaceBaseRefDiagno
|
||||
import { runnerGoalService } from "../runner-goals.js";
|
||||
import { resolveChatRunPresentationAuthorizationReason } from "../chat-run-publications.js";
|
||||
import { hasChatRunOwnedProviderInteraction } from "../chat-interaction-arbitration.js";
|
||||
import { readAcceptedMonitorContinuationSummary } from "../heartbeat-run-summary.js";
|
||||
import { readAcceptedTaskContinuationSummary } from "../heartbeat-run-summary.js";
|
||||
import { SANDBOX_PROVIDER_PLUGIN_NOT_READY_REASON } from "../recovery/stranded-notice.js";
|
||||
import { MAX_TURN_CONTINUATION_RETRY_REASON } from "../../modules/run-dispatch/index.js";
|
||||
import { redactCurrentUserText } from "../../log-redaction.js";
|
||||
@@ -789,12 +789,7 @@ export function createHeartbeatRunCompletion(db: Db, dependencies: HeartbeatRunC
|
||||
livenessRun.id,
|
||||
livenessRun.companyId,
|
||||
));
|
||||
const externalChatPresentationCandidate =
|
||||
isExternalChatPresentationContext(livenessRun.contextSnapshot) ||
|
||||
parseObject(livenessRun.contextSnapshot).source === "issue.monitor" ||
|
||||
parseObject(livenessRun.contextSnapshot).source === "tool_action_review" ||
|
||||
String(parseObject(livenessRun.contextSnapshot).source ?? "").startsWith("issue.comment") ||
|
||||
parseObject(livenessRun.contextSnapshot).source === "issue.update";
|
||||
const externalChatPresentationCandidate = isChatOriginPublicationCandidate(livenessRun.contextSnapshot);
|
||||
const externalChatPresentationAuthorization =
|
||||
issueId && externalChatPresentationCandidate
|
||||
? await measureRunPhase("response.authorize", () => resolveChatRunPresentationAuthorizationReason(db, {
|
||||
@@ -809,7 +804,7 @@ export function createHeartbeatRunCompletion(db: Db, dependencies: HeartbeatRunC
|
||||
);
|
||||
const resolved = resolveHeartbeatRunResponse({
|
||||
resultJson: persistedResultJson,
|
||||
interactionOwnsResponse: Boolean(issueId && readAcceptedMonitorContinuationSummary(persistedResultJson) &&
|
||||
interactionOwnsResponse: Boolean(issueId && readAcceptedTaskContinuationSummary(persistedResultJson) &&
|
||||
await hasChatRunOwnedProviderInteraction(db, { companyId: livenessRun.companyId, issueId, runId: livenessRun.id })),
|
||||
conversationTurnFinished: isConversation(issueContext) && livenessRun.status === "succeeded"
|
||||
&& persistedResultJson?.finalizationReasonCode === "conversation_turn_finished",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { measureRunTransaction } from "./execution-transaction-timing.js";
|
||||
import { readNativeMonitorSource } from "./native-runtime/native-monitor-delivery.js";
|
||||
import { readNativeContinuationSource } from "./native-runtime/native-continuation-delivery.js";
|
||||
import { assertNoCaseHandoffMutation, cancelCaseHandoffsForIssue, completeTerminalCaseHandoffsForIssue } from "./heartbeat/case-handoff-lifecycle.js";
|
||||
import { measureRunPhase } from "./execution-preparation-timing.js";
|
||||
import { automaticReadRecoveryCleanupCondition, directHumanCommentCondition, humanCreatedIssueCondition, pendingUserRequestCondition, touchedByUserCondition } from "./issue-participation.js";
|
||||
@@ -1397,6 +1398,12 @@ export async function resolveChatOriginPublicationBindings(
|
||||
originRunId = sourceRunId;
|
||||
continue;
|
||||
}
|
||||
if (contextSource === "native_status_decision") {
|
||||
const sourceRunId = await readNativeContinuationSource(dbOrTx, { companyId, issueId, runId: originRunId });
|
||||
if (!sourceRunId) return [];
|
||||
originRunId = sourceRunId;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
contextSource !== "issue.interaction.respond" &&
|
||||
contextSource !== "issue.interaction.accept" &&
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { agentWakeupRequests, heartbeatRuns, statusDecisions, statusDecisionEffects, type Db } from "@paperclipai/db";
|
||||
import { and, eq, inArray, ne, sql } from "drizzle-orm";
|
||||
import { alias } from "drizzle-orm/pg-core";
|
||||
import { boundRunIssueIdSql } from "../run-task-binding.js";
|
||||
|
||||
/** Recover conversation provenance from a committed continuation, never from
|
||||
* provider-supplied source IDs or the mere presence of a chat on the task. */
|
||||
export async function readNativeContinuationSource(
|
||||
db: Pick<Db, "select">,
|
||||
binding: { companyId: string; issueId: string; runId: string },
|
||||
): Promise<string | null> {
|
||||
const source = alias(heartbeatRuns, "continuation_source");
|
||||
const dispatch = alias(agentWakeupRequests, "continuation_dispatch");
|
||||
const sources = await db.select({ runId: source.id }).from(heartbeatRuns)
|
||||
.innerJoin(agentWakeupRequests, and(
|
||||
eq(agentWakeupRequests.companyId, binding.companyId),
|
||||
eq(agentWakeupRequests.runId, heartbeatRuns.id),
|
||||
eq(agentWakeupRequests.agentId, heartbeatRuns.agentId),
|
||||
eq(agentWakeupRequests.reason, "issue_status_changed"),
|
||||
eq(agentWakeupRequests.status, "coalesced"),
|
||||
eq(agentWakeupRequests.requestedByActorType, "system"),
|
||||
eq(agentWakeupRequests.requestedByActorId, "native-status-committer"),
|
||||
sql`${agentWakeupRequests.payload}->>'issueId' = ${binding.issueId}`,
|
||||
sql`${heartbeatRuns.contextSnapshot}->>'nativeStatusWakeIntentId' = ${agentWakeupRequests.id}::text`,
|
||||
))
|
||||
.innerJoin(dispatch, and(
|
||||
eq(dispatch.companyId, binding.companyId), eq(dispatch.agentId, heartbeatRuns.agentId),
|
||||
eq(dispatch.runId, heartbeatRuns.id), eq(dispatch.reason, "issue_status_changed"),
|
||||
eq(dispatch.requestedByActorType, "system"),
|
||||
sql`${dispatch.requestedByActorId} = 'native-status-wake-dispatch:' || ${agentWakeupRequests.id}::text`,
|
||||
eq(dispatch.idempotencyKey, agentWakeupRequests.idempotencyKey),
|
||||
inArray(dispatch.status, ["queued", "claimed", "completed", "coalesced"]),
|
||||
sql`${dispatch.payload}->>'issueId' = ${binding.issueId}`,
|
||||
))
|
||||
.innerJoin(statusDecisionEffects, and(
|
||||
eq(statusDecisionEffects.companyId, binding.companyId), eq(statusDecisionEffects.issueId, binding.issueId),
|
||||
eq(statusDecisionEffects.effectKind, "enqueue_continuation"),
|
||||
eq(statusDecisionEffects.targetType, "agent_wakeup_request"),
|
||||
sql`${statusDecisionEffects.targetId} = ${agentWakeupRequests.id}::text`,
|
||||
eq(statusDecisionEffects.deliveryState, "delivered"),
|
||||
sql`${statusDecisionEffects.payload}->>'continuationKind' = 'same_agent'`,
|
||||
sql`${agentWakeupRequests.payload}->>'continuationKind' = 'same_agent'`,
|
||||
))
|
||||
.innerJoin(statusDecisions, and(
|
||||
eq(statusDecisions.id, statusDecisionEffects.decisionId), eq(statusDecisions.companyId, binding.companyId),
|
||||
eq(statusDecisions.issueId, binding.issueId), eq(statusDecisions.applicationState, "applied"),
|
||||
eq(statusDecisions.reasonCode, "live_continuation_registered"),
|
||||
sql`${agentWakeupRequests.idempotencyKey} = 'native-status:' || ${statusDecisions.id}::text || ':continuation'`,
|
||||
sql`${agentWakeupRequests.payload}->>'nativeDecisionId' = ${statusDecisions.id}::text`,
|
||||
))
|
||||
.innerJoin(source, and(
|
||||
eq(source.id, statusDecisions.runId), eq(source.companyId, binding.companyId),
|
||||
eq(source.nativeIssueId, binding.issueId), eq(source.runtimeMode, "native"),
|
||||
eq(source.agentId, heartbeatRuns.agentId), ne(source.id, heartbeatRuns.id),
|
||||
sql`${source.responsibleUserId} is not distinct from ${heartbeatRuns.responsibleUserId}`,
|
||||
))
|
||||
.where(and(eq(heartbeatRuns.id, binding.runId), eq(heartbeatRuns.companyId, binding.companyId),
|
||||
eq(boundRunIssueIdSql(), binding.issueId)))
|
||||
.groupBy(source.id).limit(2);
|
||||
return sources.length === 1 ? sources[0]!.runId : null;
|
||||
}
|
||||
@@ -68,7 +68,7 @@ import {
|
||||
CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON,
|
||||
findHeartbeatRunCompletionComment,
|
||||
isExternalChatPresentationContext,
|
||||
readAcceptedMonitorContinuationSummary,
|
||||
readAcceptedTaskContinuationSummary,
|
||||
readCompletedAssistantMessageCandidate,
|
||||
resolveHeartbeatRunResponse,
|
||||
selectHeartbeatRunFinalAgentMessage,
|
||||
@@ -964,7 +964,7 @@ export async function repairCommittedNativeChatResponse(
|
||||
if (record(decision.decisionJson).externalChatReviewPresentation)
|
||||
return false;
|
||||
const monitorResult = { nativeResult: result, finalizationPhase: "committed", finalizationReasonCode: decision.reasonCode };
|
||||
if (readAcceptedMonitorContinuationSummary(monitorResult) && run.status === "succeeded" &&
|
||||
if (readAcceptedTaskContinuationSummary(monitorResult) && run.status === "succeeded" &&
|
||||
terminal.runTerminalState === "succeeded" && terminal.turnTerminalState === "completed" &&
|
||||
terminal.reportedWorkDisposition === "yielded" && record(run.contextSnapshot).skipIssueComment !== true &&
|
||||
!await hasChatRunOwnedProviderInteraction(tx, input)) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { assertNoCaseHandoffMutation } from "../heartbeat/case-handoff-lifecycle
|
||||
import { handoffCaseSchema } from "@paperclipai/shared";
|
||||
import { skillAccessService } from "../skill-access.js";
|
||||
import { withExternalExecutionAdmission, type ExternalAdmissionTransaction } from "../../modules/external-agents/index.js";
|
||||
import { callTaskWorkspaceTool } from "../task-workspace-tools.js";
|
||||
import { callTaskWorkspaceTool, pendingTaskRepositoryContinuation } from "../task-workspace-tools.js";
|
||||
import { TaskAttachmentReadCache, TASK_ATTACHMENT_DEFINITIONS, TASK_ATTACHMENT_SCHEMAS, listTaskAttachments, readTaskAttachment } from "./runner-task-attachments.js";
|
||||
import { RUNNER_BRIDGE_SCHEMAS, runnerBridgeDefinitions, readAssignedSkill, executeWorkspaceTool, readWorkspaceUploadFile, settleRunnerBridgeRead, assertRunnerWorkspacePathAllowed } from "./runner-bridge-tools.js";
|
||||
import type { NativeRuntimeContextSnapshot } from "../../vendor/paperclip-runner/index.js";
|
||||
@@ -673,7 +673,10 @@ export class PaperclipRunnerToolAuthority {
|
||||
kind: "monitor",
|
||||
summary: "Wait for the saved task monitor.",
|
||||
idempotencyKey: `task-monitor:${context.issue.id}:${monitorCheckAt}`,
|
||||
} : null,
|
||||
} : !this.binding.nativeReview && context.issue.workMode === "standard" && context.issue.executionRunId === this.binding.runId
|
||||
? await pendingTaskRepositoryContinuation(this.db, { companyId: this.binding.companyId, issueId: this.binding.issueId,
|
||||
runId: this.binding.runId, workspaceId: context.issue.executionWorkspaceId })
|
||||
: null,
|
||||
childTasks: visibleChildTasks.slice(0, 100).map(redactedTask),
|
||||
childTasksTruncated: childTasks.length > 100,
|
||||
delegationGuidance: "Reuse existing child tasks for delegated work. Inspect a completed child's comments and documents before creating another task for the same deliverable. Use search_tasks if the child task list is truncated.",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import { prepareWorkspaceRepositorySchema, selectTaskWorkspaceSchema } from "@paperclipai/shared";
|
||||
import { badRequest } from "../errors.js";
|
||||
import { executionWorkspaceRepositories, type Db } from "@paperclipai/db";
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
|
||||
export async function pendingTaskRepositoryContinuation(db: Pick<Db, "select">, input: {
|
||||
companyId: string; issueId: string; runId: string; workspaceId: string | null;
|
||||
}) {
|
||||
if (!input.workspaceId) return null;
|
||||
const [repository] = await db.select({ id: executionWorkspaceRepositories.id }).from(executionWorkspaceRepositories)
|
||||
.where(and(eq(executionWorkspaceRepositories.companyId, input.companyId),
|
||||
eq(executionWorkspaceRepositories.executionWorkspaceId, input.workspaceId),
|
||||
eq(executionWorkspaceRepositories.requestedByIssueId, input.issueId),
|
||||
eq(executionWorkspaceRepositories.state, "pending")))
|
||||
.orderBy(asc(executionWorkspaceRepositories.createdAt), asc(executionWorkspaceRepositories.id)).limit(1);
|
||||
return repository ? { kind: "same_agent" as const, summary: "Continue with the prepared task repository.",
|
||||
idempotencyKey: `task-repository:${input.issueId}:${repository.id}:${input.runId}` } : null;
|
||||
}
|
||||
|
||||
function repositoryView(repo: Record<string, unknown>) {
|
||||
return { id: repo.id, repoUrl: repo.repoUrl, relativePath: repo.relativePath, requestedRef: repo.requestedRef,
|
||||
@@ -22,7 +38,9 @@ export async function callTaskWorkspaceTool(input: { name: string; arguments: Re
|
||||
if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `Workspace tool failed (${response.status})`);
|
||||
if (input.name === "prepare_repository") {
|
||||
const { operationId, ...rest } = result;
|
||||
return { ...rest, ...(result.repository ? { repository: repositoryView(result.repository) } : {}), preparationId: operationId };
|
||||
return { ...rest, ...(result.repository ? { repository: repositoryView(result.repository) } : {}), preparationId: operationId,
|
||||
...(result.kind === "requires_next_admission" ? { instruction: "The repository is staged for the next admission. If unfinished work needs this checkout, finish independent work and yield. Paperclip will select the saved workspace continuation; no timer or user restart is needed." } : {}),
|
||||
};
|
||||
}
|
||||
if (input.name === "get_workspace") {
|
||||
const workspace = result.workspace;
|
||||
|
||||
@@ -155,6 +155,7 @@ If `currentParticipant` does not match you, do not try to advance the stage —
|
||||
**Step 7 — Do the work.** Use your tools and capabilities. Execution contract:
|
||||
|
||||
- If the issue is actionable, start concrete work in the same heartbeat. Do not stop at a plan unless the issue specifically asks for planning.
|
||||
- Ask for information only when it prevents the next useful action. Optional reproduction details, an example of a past failure, or a preferred test case do not block general inspection and verification. Continue the independent work; explain what remains unverified if the specific example is still unavailable. Keep real access restrictions and approval gates authoritative.
|
||||
- Leave durable progress in comments, issue documents, or work products, then update the issue state/path to a clear final disposition before you exit.
|
||||
- Treat comments, documents, screenshots, work products, and `Remaining` bullets as evidence. They are not valid liveness paths by themselves.
|
||||
- Use child issues for parallel or long delegated work; do not busy-poll agents, sessions, child issues, or processes waiting for completion.
|
||||
@@ -552,6 +553,8 @@ Use the task's provided working directory for task files. `AGENT_HOME` holds per
|
||||
|
||||
Use `get_workspace` to inspect the current folder, pending selection, and capabilities. Use `list_workspaces` for authorized existing folders and `list_project_repositories` for repository choices. `select_workspace` and `prepare_repository` express intent; the harness validates and prepares it. Respect `next_normal_admission` responses: keep the current root, do not claim the requested checkout exists, and do not retry in a loop or interrupt the run. Project assignment alone does not relocate files. See `references/issue-workspaces.md` for the API equivalents.
|
||||
|
||||
Prefer a matching configured, authorized source over guessing a repository URL. Inspect available runtime and testing capabilities before claiming that browser or command testing is unavailable. When an authorized repository is pending and unfinished work needs it, finish independent work, explain the remaining work, and yield. Native Runner can select the saved same-agent continuation from current task context; it does not need a timer or a user restart. A request only to stage a repository can finish normally without starting more work.
|
||||
|
||||
## Issue Workspace Runtime Controls
|
||||
|
||||
When an issue needs browser/manual QA or a preview server, inspect its current execution workspace and use Paperclip's workspace runtime controls instead of starting unmanaged background servers yourself.
|
||||
|
||||
@@ -58,6 +58,7 @@ export const DAYTONA_IMAGE_INPUT_PATHS = [
|
||||
"packages/paperclip-runner/test/fixtures/copilot-profile-v47-identity.json",
|
||||
"packages/paperclip-runner/test/fixtures/copilot-profile-v48-identity.json",
|
||||
"packages/paperclip-runner/test/fixtures/copilot-profile-v49-identity.json",
|
||||
"packages/paperclip-runner/test/fixtures/copilot-profile-v53-identity.json",
|
||||
"packages/paperclip-runner/scripts/materialize-cursor-distribution.mjs",
|
||||
"packages/paperclip-runner/scripts/cursor-runtime-patch.mjs",
|
||||
"packages/paperclip-runner/scripts/build-verified-provider-entrypoints.mjs",
|
||||
|
||||
Reference in new issue
Block a user