@agent-plan/core 0.2.29 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accepted-decision-guard.d.ts +94 -0
- package/dist/accepted-decision-guard.d.ts.map +1 -0
- package/dist/accepted-decision-guard.js +110 -0
- package/dist/checklist.d.ts +28 -0
- package/dist/checklist.d.ts.map +1 -1
- package/dist/checklist.js +59 -0
- package/dist/feature-delete.d.ts +62 -0
- package/dist/feature-delete.d.ts.map +1 -0
- package/dist/feature-delete.js +49 -0
- package/dist/guard-decision.d.ts +128 -0
- package/dist/guard-decision.d.ts.map +1 -0
- package/dist/guard-decision.js +269 -0
- package/dist/guard-state.d.ts +14 -0
- package/dist/guard-state.d.ts.map +1 -0
- package/dist/guard-state.js +35 -0
- package/dist/handoff-context.d.ts +130 -1
- package/dist/handoff-context.d.ts.map +1 -1
- package/dist/handoff-context.js +167 -9
- package/dist/handoff-reply.d.ts +113 -0
- package/dist/handoff-reply.d.ts.map +1 -0
- package/dist/handoff-reply.js +279 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -1
- package/dist/load-reply.d.ts +63 -0
- package/dist/load-reply.d.ts.map +1 -0
- package/dist/load-reply.js +64 -0
- package/dist/mutation-reply.d.ts +105 -0
- package/dist/mutation-reply.d.ts.map +1 -0
- package/dist/mutation-reply.js +203 -0
- package/dist/naming.d.ts +1 -1
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +1 -1
- package/dist/on-hold.d.ts +55 -0
- package/dist/on-hold.d.ts.map +1 -0
- package/dist/on-hold.js +123 -0
- package/dist/package-version.d.ts +13 -0
- package/dist/package-version.d.ts.map +1 -1
- package/dist/package-version.js +32 -0
- package/dist/pagination.d.ts +19 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +15 -0
- package/dist/payload-fallback.js +1 -1
- package/dist/plan-store.d.ts +141 -8
- package/dist/plan-store.d.ts.map +1 -1
- package/dist/plan-store.js +459 -87
- package/dist/project-context-migration.d.ts +5 -0
- package/dist/project-context-migration.d.ts.map +1 -1
- package/dist/project-context-migration.js +13 -0
- package/dist/project-context.d.ts +397 -0
- package/dist/project-context.d.ts.map +1 -0
- package/dist/project-context.js +294 -0
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +33 -0
- package/dist/recommend-reply.d.ts +51 -0
- package/dist/recommend-reply.d.ts.map +1 -0
- package/dist/recommend-reply.js +93 -0
- package/dist/refs.d.ts +16 -0
- package/dist/refs.d.ts.map +1 -1
- package/dist/refs.js +38 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/schema.d.ts +7 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +8 -3
- package/dist/stranded-phase.d.ts +73 -0
- package/dist/stranded-phase.d.ts.map +1 -0
- package/dist/stranded-phase.js +93 -0
- package/dist/task-completion-gate.d.ts +84 -0
- package/dist/task-completion-gate.d.ts.map +1 -0
- package/dist/task-completion-gate.js +72 -0
- package/dist/task-context.d.ts +1 -0
- package/dist/task-context.d.ts.map +1 -1
- package/dist/task-context.js +42 -14
- package/dist/task-lifecycle.d.ts +12 -0
- package/dist/task-lifecycle.d.ts.map +1 -0
- package/dist/task-lifecycle.js +23 -0
- package/dist/task-order-context.d.ts +72 -0
- package/dist/task-order-context.d.ts.map +1 -0
- package/dist/task-order-context.js +82 -0
- package/dist/task-selection.d.ts +4 -1
- package/dist/task-selection.d.ts.map +1 -1
- package/dist/task-selection.js +36 -14
- package/dist/task-start-outcome.d.ts +26 -0
- package/dist/task-start-outcome.d.ts.map +1 -1
- package/dist/task-start-outcome.js +23 -0
- package/dist/text-bounds.d.ts +32 -0
- package/dist/text-bounds.d.ts.map +1 -0
- package/dist/text-bounds.js +43 -0
- package/dist/write-coordination.d.ts +2 -2
- package/dist/write-coordination.js +2 -2
- package/package.json +1 -1
- package/planner-skill.md +30 -12
package/dist/handoff-context.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { buildPhaseWorkMap } from "./task-context.js";
|
|
3
|
+
import { truncateAtSafeBoundary } from "./text-bounds.js";
|
|
3
4
|
export const COMPLETION_SUMMARY_HEADING = "**Completion summary:**";
|
|
4
5
|
export const HANDOFF_COMPLETENESS_AUDIT_VERSION = 1;
|
|
5
6
|
export const HANDOFF_COLD_START_INVENTORY_VERSION = 1;
|
|
@@ -68,6 +69,121 @@ export class HandoffContractError extends Error {
|
|
|
68
69
|
this.details = details;
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
/** Single source of truth for the supportingDocuments recovery statement.
|
|
73
|
+
* Rendered in prepare output (before drafting) and attached to every
|
|
74
|
+
* agent-supplied-manifest failure of HANDOFF_SUPPORTING_DOCUMENT_INVALID
|
|
75
|
+
* (as details.recovery) so both adapters can render it without duplicating
|
|
76
|
+
* the wording. */
|
|
77
|
+
export const HANDOFF_SUPPORTING_DOCUMENTS_GUIDANCE = "supportingDocuments is optional and usually unnecessary: content above the target length is externalized automatically into .planner/docs/. Drop the field and retry unless you are linking a document you created yourself.";
|
|
78
|
+
/** Build a HANDOFF_SUPPORTING_DOCUMENT_INVALID error for a defect in an
|
|
79
|
+
* agent-supplied manifest entry, always carrying the droppable-field
|
|
80
|
+
* recovery statement. Do not use this for planner-owned failures (e.g. the
|
|
81
|
+
* auto-externalized document's own path) — those are not something dropping
|
|
82
|
+
* the agent's field would fix. */
|
|
83
|
+
export function supportingDocumentInvalidError(message, details = {}) {
|
|
84
|
+
return new HandoffContractError("HANDOFF_SUPPORTING_DOCUMENT_INVALID", message, { ...details, recovery: HANDOFF_SUPPORTING_DOCUMENTS_GUIDANCE });
|
|
85
|
+
}
|
|
86
|
+
/** Build a HANDOFF_SUPPORTING_DOCUMENT_INVALID error for a PlanStore-internal
|
|
87
|
+
* invariant (the count of requested vs. PlanStore-verified documents must
|
|
88
|
+
* always match; PlanStore either throws for a defective entry or returns one
|
|
89
|
+
* verified entry per requested document, in lockstep). This is unreachable
|
|
90
|
+
* through the documented tool flow, so unlike supportingDocumentInvalidError
|
|
91
|
+
* it must not carry the droppable-field recovery statement: dropping the
|
|
92
|
+
* field would not be the fix for an invariant violation, and advertising it
|
|
93
|
+
* would mislead the agent. */
|
|
94
|
+
function supportingDocumentInvariantError(message, details = {}) {
|
|
95
|
+
return new HandoffContractError("HANDOFF_SUPPORTING_DOCUMENT_INVALID", message, details);
|
|
96
|
+
}
|
|
97
|
+
/** Build a HANDOFF_RETAINED_CONTENT_NOT_FOUND error: `content` was omitted
|
|
98
|
+
* from a handoff_write call and PlanStore has no body retained for this
|
|
99
|
+
* phase + expectedHandoffUpdatedAt token (see PlanStore.retainHandoffDraft /
|
|
100
|
+
* getRetainedHandoffDraft). Reasons: no write against this token has ever
|
|
101
|
+
* failed, the retained draft aged out, the phase went terminal and its
|
|
102
|
+
* drafts were discarded, or the token itself is wrong/stale. Content is
|
|
103
|
+
* always required on the first write attempt for a token; it becomes
|
|
104
|
+
* optional only on a retry that reuses a retained one. */
|
|
105
|
+
export function retainedHandoffContentNotFoundError(phaseId, expectedHandoffUpdatedAt) {
|
|
106
|
+
return new HandoffContractError("HANDOFF_RETAINED_CONTENT_NOT_FOUND", "content was omitted but no retained draft was found for this phase and expectedHandoffUpdatedAt token.", {
|
|
107
|
+
phaseId,
|
|
108
|
+
expectedHandoffUpdatedAt,
|
|
109
|
+
recovery: "Provide content once. After a write fails, a retry against the exact same expectedHandoffUpdatedAt may omit content to reuse the body just submitted; a fresh token (from a new handoff_prepare) or a phase that has since gone terminal has nothing retained.",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** The exact description externalizeOversizedHandoffContent stamps on the
|
|
113
|
+
* supporting-document entry it generates for an auto-externalized document.
|
|
114
|
+
* The single source of truth for recognizing one later: handoff_prepare
|
|
115
|
+
* matches on this instead of the timestamped file-naming convention, so
|
|
116
|
+
* detection and generation cannot drift apart. A document a human or agent
|
|
117
|
+
* linked deliberately (a different description) is already named in the
|
|
118
|
+
* persisted capsule's own "Supporting documents" bullet and is out of scope
|
|
119
|
+
* here — only content the tooling itself moved out without being asked. */
|
|
120
|
+
export const HANDOFF_AUTO_EXTERNALIZED_DOCUMENT_DESCRIPTION = "Full submitted handoff detail externalized automatically; required for cold resume and reconciliation.";
|
|
121
|
+
export function isAutoExternalizedHandoffDocument(document) {
|
|
122
|
+
return document.description === HANDOFF_AUTO_EXTERNALIZED_DOCUMENT_DESCRIPTION;
|
|
123
|
+
}
|
|
124
|
+
/** Level-2 (`##`) headings of a handoff document, in order. Used to name
|
|
125
|
+
* what an externalized document contains when its content cannot be
|
|
126
|
+
* inlined (budget, or the file is missing) and as a table of contents
|
|
127
|
+
* alongside content that is inlined. */
|
|
128
|
+
export function extractHandoffSectionHeadings(content) {
|
|
129
|
+
const headings = [];
|
|
130
|
+
for (const line of content.split(/\r?\n/)) {
|
|
131
|
+
const match = line.match(/^##\s+(.+?)\s*$/);
|
|
132
|
+
if (match)
|
|
133
|
+
headings.push(match[1]);
|
|
134
|
+
}
|
|
135
|
+
return headings;
|
|
136
|
+
}
|
|
137
|
+
/** Prior `##` headings with no counterpart heading in `newContent`. A
|
|
138
|
+
* section counts as carried forward only if its exact heading text
|
|
139
|
+
* (case-insensitive) still appears as a `##` heading somewhere in the new
|
|
140
|
+
* content — matching by heading, not by scanning prose for the words, keeps
|
|
141
|
+
* this a narrow structural check rather than a fuzzy content audit. Callers
|
|
142
|
+
* needing "did the author account for this" evidence use
|
|
143
|
+
* validateHandoffSectionReconciliation below, which turns this list into a
|
|
144
|
+
* typed refusal unless every entry has a disposition. */
|
|
145
|
+
function unaccountedHandoffSections(priorSectionHeadings, newContent) {
|
|
146
|
+
const newHeadings = new Set(extractHandoffSectionHeadings(newContent).map((heading) => heading.trim().toLowerCase()));
|
|
147
|
+
return priorSectionHeadings.filter((heading) => !newHeadings.has(heading.trim().toLowerCase()));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Make reconciliation evidenced rather than asserted (P104(F005)/T416).
|
|
151
|
+
*
|
|
152
|
+
* Narrow by design: this compares `##` heading text only, never section
|
|
153
|
+
* content or a fixed category list — it is not a return to the 16-section
|
|
154
|
+
* scaffold and 14-category cold-start inventory P100(F021)/T396 removed for
|
|
155
|
+
* making authoring verbose and failing late. A prior capsule with no
|
|
156
|
+
* headings (or a new submission with none) simply has nothing to name here;
|
|
157
|
+
* the check degrades to a no-op rather than blocking the write.
|
|
158
|
+
*
|
|
159
|
+
* A prior section vanishes from the new content in three ways an author
|
|
160
|
+
* might legitimately intend: dropped because it no longer matters,
|
|
161
|
+
* superseded by newer content, or folded into a different section (a rename
|
|
162
|
+
* reads the same way — the old heading is gone, the content lives under a
|
|
163
|
+
* new one). This function does not try to tell those apart; it only
|
|
164
|
+
* requires the author to say which happened, per named section, before the
|
|
165
|
+
* write proceeds. A deliberate full rewrite where every prior section is
|
|
166
|
+
* obsolete is handled the same way: every heading is named, and the author
|
|
167
|
+
* writes one disposition per heading.
|
|
168
|
+
*/
|
|
169
|
+
export function validateHandoffSectionReconciliation(priorSectionHeadings, newContent, dispositions) {
|
|
170
|
+
const unaccounted = unaccountedHandoffSections(priorSectionHeadings, newContent);
|
|
171
|
+
if (unaccounted.length === 0)
|
|
172
|
+
return;
|
|
173
|
+
const bySection = new Map();
|
|
174
|
+
for (const entry of dispositions ?? []) {
|
|
175
|
+
const section = entry.section?.trim();
|
|
176
|
+
if (section)
|
|
177
|
+
bySection.set(section.toLowerCase(), entry.disposition ?? "");
|
|
178
|
+
}
|
|
179
|
+
const stillUnaccounted = unaccounted.filter((section) => !isSubstantive(bySection.get(section.trim().toLowerCase()) ?? ""));
|
|
180
|
+
if (stillUnaccounted.length > 0) {
|
|
181
|
+
throw new HandoffContractError("HANDOFF_SECTION_RECONCILIATION_REQUIRED", "Prior handoff sections have no counterpart in the new content. Supply sectionDispositions with one substantive entry per named section (dropped, superseded, or folded into another section) before writing.", {
|
|
182
|
+
unaccountedSections: stillUnaccounted,
|
|
183
|
+
recovery: "Retry against the same phaseRef and expectedHandoffUpdatedAt with sectionDispositions only; content may be omitted to reuse the body just submitted.",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
71
187
|
export function hasTaskCompletionEvidence(task) {
|
|
72
188
|
if (task.status !== "done")
|
|
73
189
|
return true;
|
|
@@ -132,6 +248,13 @@ export function auditPhaseHandoff(phase, feature) {
|
|
|
132
248
|
coldStartInventoryCategories: HANDOFF_COLD_START_INVENTORY_CATEGORIES,
|
|
133
249
|
existingCompletenessAudit: phase.handoffAudit,
|
|
134
250
|
phaseWorkMap: buildPhaseWorkMap(phase, feature.number),
|
|
251
|
+
supportingDocumentsGuidance: HANDOFF_SUPPORTING_DOCUMENTS_GUIDANCE,
|
|
252
|
+
// Filled in by PlanStore.preparePhaseHandoff, which alone can read
|
|
253
|
+
// .planner/docs/; this pure function has no filesystem access.
|
|
254
|
+
externalizedHandoffDocuments: [],
|
|
255
|
+
// Compact-capsule headings only; PlanStore.preparePhaseHandoff overlays
|
|
256
|
+
// headings from auto-externalized documents once it has read them back.
|
|
257
|
+
priorSectionHeadings: phase.handoff.trim() ? extractHandoffSectionHeadings(phase.handoff) : [],
|
|
135
258
|
};
|
|
136
259
|
}
|
|
137
260
|
function nonEmpty(value, field) {
|
|
@@ -339,11 +462,16 @@ function sectionBody(content, headings) {
|
|
|
339
462
|
}
|
|
340
463
|
return body.join("\n").trim();
|
|
341
464
|
}
|
|
465
|
+
// truncateAtSafeBoundary / boundedContentForTransport moved to text-bounds.ts
|
|
466
|
+
// (P104(F005)/T419) so task-context.ts can share them without creating a
|
|
467
|
+
// cycle: this module already imports buildPhaseWorkMap from task-context.ts.
|
|
468
|
+
// Both are exported directly from text-bounds.js at the package root now.
|
|
342
469
|
function boundedSection(value, fallback, maxChars) {
|
|
343
470
|
const normalized = value.trim() || fallback;
|
|
344
471
|
if (normalized.length <= maxChars)
|
|
345
472
|
return normalized;
|
|
346
|
-
|
|
473
|
+
const safe = truncateAtSafeBoundary(normalized, Math.max(0, maxChars - 86));
|
|
474
|
+
return `${safe}\n\n[Extended detail continues in the linked planner document.]`;
|
|
347
475
|
}
|
|
348
476
|
/**
|
|
349
477
|
* Mechanically compact oversized legacy or verbose handoffs while retaining the
|
|
@@ -390,7 +518,7 @@ export function externalizeOversizedHandoffContent(content, supportingDocumentPa
|
|
|
390
518
|
extendedContent: `${firstHeading} — extended detail\n\n${base}\n`,
|
|
391
519
|
supportingDocument: {
|
|
392
520
|
path: supportingDocumentPath,
|
|
393
|
-
description:
|
|
521
|
+
description: HANDOFF_AUTO_EXTERNALIZED_DOCUMENT_DESCRIPTION,
|
|
394
522
|
},
|
|
395
523
|
};
|
|
396
524
|
}
|
|
@@ -456,20 +584,32 @@ export function validateHandoffContextSync(phase, feature, input) {
|
|
|
456
584
|
const requestedDocuments = input.supportingDocuments ?? [];
|
|
457
585
|
const verifiedDocuments = input.verifiedSupportingDocuments ?? [];
|
|
458
586
|
if (requestedDocuments.length !== verifiedDocuments.length) {
|
|
459
|
-
throw
|
|
587
|
+
throw supportingDocumentInvariantError("Every supporting document must be validated by PlanStore before the handoff is written.", { requestedCount: requestedDocuments.length, verifiedCount: verifiedDocuments.length });
|
|
460
588
|
}
|
|
461
589
|
const verifiedContents = input.verifiedSupportingDocumentContents ?? [];
|
|
462
590
|
if (verifiedContents.length !== verifiedDocuments.length) {
|
|
463
|
-
throw
|
|
591
|
+
throw supportingDocumentInvariantError("Validated supporting-document content is required for cold-start inventory coverage checks.", { verifiedDocumentCount: verifiedDocuments.length, verifiedContentCount: verifiedContents.length });
|
|
464
592
|
}
|
|
465
593
|
for (let index = 0; index < requestedDocuments.length; index += 1) {
|
|
466
594
|
const requested = requestedDocuments[index];
|
|
467
595
|
const verified = verifiedDocuments[index];
|
|
468
596
|
if (requested.path !== verified.path || !isSubstantive(requested.description) || requested.description.trim() !== verified.description) {
|
|
469
|
-
throw
|
|
597
|
+
throw supportingDocumentInvalidError(`Supporting document ${requested.path || `(index ${index})`} is not valid or lacks a substantive description.`, { index, path: requested.path });
|
|
470
598
|
}
|
|
471
|
-
|
|
472
|
-
|
|
599
|
+
// Body-dependent: the manifest is valid on its own, but the drafted content
|
|
600
|
+
// never linked it. This is the one check that cannot move to prepare, since
|
|
601
|
+
// prepare runs before the body exists.
|
|
602
|
+
//
|
|
603
|
+
// Check the submitted body as well as the persisted one. Auto-externalization
|
|
604
|
+
// rewrites `content` after the agent submitted it, moving whole sections into
|
|
605
|
+
// a .planner/docs/ file; a path the author linked inside one of those sections
|
|
606
|
+
// is no longer in `content` through no fault of theirs. The planner's own
|
|
607
|
+
// auto-externalized document is the mirror case — its path is generated during
|
|
608
|
+
// the rewrite, so it appears only in `content`. Accepting either body covers
|
|
609
|
+
// both without letting through a path the agent never linked anywhere.
|
|
610
|
+
const linkedBodies = [input.content, ...(input.submittedContent ? [input.submittedContent] : [])];
|
|
611
|
+
if (!linkedBodies.some((body) => body.includes(requested.path))) {
|
|
612
|
+
throw supportingDocumentInvalidError(`Canonical handoff content must link supporting document ${requested.path}. Checked the ${input.submittedContent ? "submitted body and the auto-externalized body" : "submitted body"}.`, { index, path: requested.path });
|
|
473
613
|
}
|
|
474
614
|
}
|
|
475
615
|
renderVerifiedHandoffContent(input.content, input.completenessAudit, input.coldStartInventory, verifiedContents);
|
|
@@ -482,6 +622,15 @@ export function validateHandoffContextSync(phase, feature, input) {
|
|
|
482
622
|
if (phase.handoff.trim() && !input.reconciledExistingHandoff) {
|
|
483
623
|
throw new Error("An active handoff already exists. Reconcile its still-relevant content and set reconciledExistingHandoff=true.");
|
|
484
624
|
}
|
|
625
|
+
if (phase.handoff.trim()) {
|
|
626
|
+
// Judge against the body the author actually wrote, not the planner's
|
|
627
|
+
// own auto-externalized compaction (mirrors the supporting-document link
|
|
628
|
+
// check above): when this write itself got compacted, submittedContent
|
|
629
|
+
// is the full pre-compaction text and is what a prior section's heading
|
|
630
|
+
// would still appear in.
|
|
631
|
+
const newContentForHeadings = input.submittedContent ?? input.content;
|
|
632
|
+
validateHandoffSectionReconciliation(input.priorSectionHeadings ?? [], newContentForHeadings, input.sectionDispositions);
|
|
633
|
+
}
|
|
485
634
|
const sync = input.contextSync;
|
|
486
635
|
const taskIds = sync.taskUpdates.map((update) => nonEmpty(update.taskId, "Task update taskId"));
|
|
487
636
|
if (new Set(taskIds).size !== taskIds.length)
|
|
@@ -528,6 +677,14 @@ export function applyHandoffContextSync(phase, feature, input, timestamp) {
|
|
|
528
677
|
for (const update of input.contextSync.taskUpdates) {
|
|
529
678
|
const task = nextPhase.tasks.find((candidate) => candidate.id === update.taskId);
|
|
530
679
|
const files = uniqueStrings(update.filesTouched ?? []);
|
|
680
|
+
// Decisions mentioned in a completion summary are prose, not decision
|
|
681
|
+
// authority (a completion summary is explicitly one of the places a new
|
|
682
|
+
// durable decision must not live only in). Render them into the section
|
|
683
|
+
// text with that caveat instead of appending to the legacy task.decisions
|
|
684
|
+
// array, which nothing treats as authoritative and which is otherwise
|
|
685
|
+
// read-only going forward (see accepted-decision-guard.ts). A decision
|
|
686
|
+
// worth keeping needs its own accepted_decision_create call on the right
|
|
687
|
+
// owner; this list is not a substitute for that.
|
|
531
688
|
const decisions = uniqueStrings(update.decisions ?? []);
|
|
532
689
|
const section = [
|
|
533
690
|
COMPLETION_SUMMARY_HEADING,
|
|
@@ -539,24 +696,25 @@ export function applyHandoffContextSync(phase, feature, input, timestamp) {
|
|
|
539
696
|
"**Remaining or unverified:**",
|
|
540
697
|
update.remainingWork.trim(),
|
|
541
698
|
...(files.length > 0 ? ["", "**Files touched:**", ...files.map((file) => `- ${file}`)] : []),
|
|
699
|
+
...(decisions.length > 0 ? ["", "**Decisions mentioned (not decision authority; record any durable decision with accepted_decision_create on its actual owner):**", ...decisions.map((decision) => `- ${decision}`)] : []),
|
|
542
700
|
].join("\n");
|
|
543
701
|
task.description = appendSection(task.description, section);
|
|
544
702
|
task.descriptionUpdatedAt = timestamp;
|
|
545
|
-
task.decisions = uniqueStrings([...(task.decisions ?? []), ...decisions]);
|
|
546
703
|
task.updatedAt = timestamp;
|
|
547
704
|
updatedTaskIds.push(task.id);
|
|
548
705
|
}
|
|
549
706
|
const phaseUpdate = input.contextSync.phaseUpdate;
|
|
550
707
|
if (phaseUpdate) {
|
|
708
|
+
const phaseDecisions = uniqueStrings(phaseUpdate.decisions ?? []);
|
|
551
709
|
const section = [
|
|
552
710
|
"**Handoff context update:**",
|
|
553
711
|
phaseUpdate.progressSummary.trim(),
|
|
554
712
|
"",
|
|
555
713
|
"**Remaining work:**",
|
|
556
714
|
phaseUpdate.remainingWork.trim(),
|
|
715
|
+
...(phaseDecisions.length > 0 ? ["", "**Decisions mentioned (not decision authority; record any durable decision with accepted_decision_create on its actual owner):**", ...phaseDecisions.map((decision) => `- ${decision}`)] : []),
|
|
557
716
|
].join("\n");
|
|
558
717
|
nextPhase.notes = appendSection(nextPhase.notes, section);
|
|
559
|
-
nextPhase.decisions = uniqueStrings([...(nextPhase.decisions ?? []), ...(phaseUpdate.decisions ?? [])]);
|
|
560
718
|
}
|
|
561
719
|
const featureUpdate = input.contextSync.featureUpdate;
|
|
562
720
|
if (featureUpdate) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reply shaping for the handoff show/prepare tools.
|
|
3
|
+
*
|
|
4
|
+
* Every adapter (MCP, Pi, and any future harness) calls the same builders
|
|
5
|
+
* here instead of assembling its own text/structuredContent pair. Each
|
|
6
|
+
* builder returns exactly one copy of every piece of information: the
|
|
7
|
+
* human-readable channel (`text`) and the machine-readable channel
|
|
8
|
+
* (`structured`) never both carry the same field. Which channel is
|
|
9
|
+
* canonical for a given piece of content is decided per field below, based
|
|
10
|
+
* on what a structuredContent-only host needs in order to act (see the
|
|
11
|
+
* per-field comments) rather than by a single blanket rule — see AGENTS.md
|
|
12
|
+
* rule 4: this placement decision belongs in the core exactly once, not
|
|
13
|
+
* once per adapter.
|
|
14
|
+
*
|
|
15
|
+
* Transport note: Claude Code shows the agent only structuredContent when a
|
|
16
|
+
* result has one, so the MCP server folds `text` into the structured part
|
|
17
|
+
* for every tool (foldTextIntoStructured in plan-mcp). The rule above still
|
|
18
|
+
* holds for what these builders return; the fold is the MCP transport's job.
|
|
19
|
+
*/
|
|
20
|
+
import type { HandoffCompletenessAudit, Phase } from "./schema.js";
|
|
21
|
+
import type { PhaseWorkMap } from "./task-context.js";
|
|
22
|
+
import { type HandoffContractError, type PhaseHandoffAudit } from "./handoff-context.js";
|
|
23
|
+
import type { MutationReply } from "./mutation-reply.js";
|
|
24
|
+
/**
|
|
25
|
+
* Render a HandoffContractError (handoff refresh/prepare denial) as a
|
|
26
|
+
* text/structured pair. Both adapters carried an identical copy; kept here
|
|
27
|
+
* so the message and the `errorCode`/details shape can never drift between
|
|
28
|
+
* MCP and Pi. Each adapter still owns its own envelope (`isError` +
|
|
29
|
+
* `structuredContent` for MCP, `details` for Pi) — see AGENTS.md rule 4.
|
|
30
|
+
*/
|
|
31
|
+
export declare function handoffContractFailureReply(error: HandoffContractError, action?: "refresh" | "preparation"): MutationReply;
|
|
32
|
+
export interface BoundedHandoffContent {
|
|
33
|
+
content: string;
|
|
34
|
+
fullLength: number;
|
|
35
|
+
truncated: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Bound raw handoff content to the absolute compatibility ceiling for
|
|
38
|
+
* transport. Legacy or externally-written content can exceed the ceiling;
|
|
39
|
+
* everything produced through the normal write path already respects it.
|
|
40
|
+
* Cuts at a safe boundary (paragraph, then line, then word — see
|
|
41
|
+
* boundedContentForTransport / truncateAtSafeBoundary in handoff-context.ts)
|
|
42
|
+
* rather than an arbitrary character offset, so the visible remainder never
|
|
43
|
+
* ends mid-word. */
|
|
44
|
+
export declare function boundedHandoffForTransport(content: string): BoundedHandoffContent;
|
|
45
|
+
/** A tool reply as one result: the human-readable body and the structured
|
|
46
|
+
* payload, with no field present in both. */
|
|
47
|
+
export interface HandoffReply {
|
|
48
|
+
text: string;
|
|
49
|
+
structured: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
export type HandoffShowReplyInput = {
|
|
52
|
+
kind: "empty";
|
|
53
|
+
phaseRef: string;
|
|
54
|
+
phaseId: string;
|
|
55
|
+
handoffAudit: HandoffCompletenessAudit | null;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "archived";
|
|
58
|
+
phaseRef: string;
|
|
59
|
+
phaseId: string;
|
|
60
|
+
/** Raw archived content; the builder bounds it for transport. */
|
|
61
|
+
content: string;
|
|
62
|
+
archiveReason: string;
|
|
63
|
+
archivedAt: string;
|
|
64
|
+
archiveFile: string;
|
|
65
|
+
handoffAudit: HandoffCompletenessAudit | null;
|
|
66
|
+
} | {
|
|
67
|
+
kind: "active";
|
|
68
|
+
phaseRef: string;
|
|
69
|
+
phase: Phase;
|
|
70
|
+
phaseWorkMap: PhaseWorkMap;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Build the reply for `planner-handoff-show` / `handoff_show`, covering the
|
|
74
|
+
* empty, archived, and active branches through one shared shape so no
|
|
75
|
+
* surface keeps its own construction.
|
|
76
|
+
*
|
|
77
|
+
* Placement: the resume capsule body is prose meant to be read, and every
|
|
78
|
+
* existing consumer reads it from `text` (never from `structured`), so
|
|
79
|
+
* `text` is its one home; `structured` carries only `fullLength`/
|
|
80
|
+
* `truncated` flags plus the identifiers, hash, and verification flags a
|
|
81
|
+
* structuredContent-only host needs to drive the candidate-then-verify
|
|
82
|
+
* contract (call verify with the right hash) without needing the prose
|
|
83
|
+
* itself.
|
|
84
|
+
*/
|
|
85
|
+
export declare function buildHandoffShowReply(input: HandoffShowReplyInput): HandoffReply;
|
|
86
|
+
/** Single source of truth for the prepare-reply evidence-contract sentence,
|
|
87
|
+
* previously hardcoded identically in both adapters. */
|
|
88
|
+
export declare const HANDOFF_PREPARE_EVIDENCE_CONTRACT = "Planner-owned completeness and cold-start evidence is derived from persisted state; no category inventory is required from the agent.";
|
|
89
|
+
export interface HandoffPrepareReplyInput {
|
|
90
|
+
phaseRef: string;
|
|
91
|
+
audit: PhaseHandoffAudit;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Build the reply for `planner-handoff-prepare` / `handoff_prepare`.
|
|
95
|
+
*
|
|
96
|
+
* Placement: `draftTemplate` has no consumer in `text` (only the intro line
|
|
97
|
+
* pointing at it is read there) and every existing consumer reads it from
|
|
98
|
+
* `structured`, so `structured` is its one home; `text` gets a pointer
|
|
99
|
+
* instead of a second copy. The existing active handoff being reconciled is
|
|
100
|
+
* the opposite: nothing reads it from `structured`, so it stays prose in
|
|
101
|
+
* `text` only, bounded for transport, with `structured` carrying just its
|
|
102
|
+
* length/truncated flags. `missingCompletionTasks` duplicates the same
|
|
103
|
+
* titles already rendered into `text`; `structured` keeps only the task ids
|
|
104
|
+
* needed to address `taskUpdates`. The existing handoff's auto-externalized
|
|
105
|
+
* documents follow the same rule as the existing handoff itself: their
|
|
106
|
+
* bodies are prose for `text` (see renderExternalizedHandoffDocuments),
|
|
107
|
+
* `structured` keeps only path/headings/size/inlined/truncated per document
|
|
108
|
+
* (existingHandoffExternalizedDocuments) — see P104(F005)/T415's accepted
|
|
109
|
+
* decision for why inlining (budget permitting) was chosen over a
|
|
110
|
+
* separate-field or names-only reply.
|
|
111
|
+
*/
|
|
112
|
+
export declare function buildHandoffPrepareReply(input: HandoffPrepareReplyInput): HandoffReply;
|
|
113
|
+
//# sourceMappingURL=handoff-reply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff-reply.d.ts","sourceRoot":"","sources":["../src/handoff-reply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAA6B,KAAK,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,oBAAoB,EAAE,MAAM,GAAE,SAAS,GAAG,aAAyB,GAAG,aAAa,CAMrI;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;oBAMoB;AACpB,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CAKjF;AA8CD;6CAC6C;AAC7C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAmCD,MAAM,MAAM,qBAAqB,GAC7B;IACE,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAAC;CAC/C,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAAC;CAC/C,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;CAC5B,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,qBAAqB,GAAG,YAAY,CAsEhF;AAED;wDACwD;AACxD,eAAO,MAAM,iCAAiC,0IAC2F,CAAC;AAE1I,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,wBAAwB,GAAG,YAAY,CAkFtF"}
|