@mgiles/perk 2.3.0 → 3.1.0
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/README.md +1 -1
- package/extension/adapters/planAdapterPlannotator.ts +135 -91
- package/extension/adapters/planAdapterTombell.ts +5 -3
- package/extension/doors/address.ts +235 -37
- package/extension/doors/annotationPush.ts +924 -0
- package/extension/doors/auditWaveTools.ts +352 -0
- package/extension/doors/ciExecutor.ts +220 -55
- package/extension/doors/commitCompact.ts +98 -10
- package/extension/doors/draftReviewWaveTools.ts +412 -0
- package/extension/doors/dreamWaveTools.ts +475 -0
- package/extension/doors/harvestWaveTools.ts +279 -0
- package/extension/doors/land.ts +1 -0
- package/extension/doors/learn.ts +24 -6
- package/extension/doors/learnFactory.ts +33 -7
- package/extension/doors/lifecycleGates.ts +1 -1
- package/extension/doors/objectiveReviewBrowser.ts +537 -0
- package/extension/doors/objectiveStack.ts +1143 -0
- package/extension/doors/planReviewBrowser.ts +475 -0
- package/extension/doors/plannotatorHandoff.ts +120 -49
- package/extension/doors/prReview.ts +180 -63
- package/extension/doors/prReviewBrowser.ts +31 -23
- package/extension/doors/prReviewDynamic.ts +57 -30
- package/extension/doors/prReviewTerminal.ts +6 -11
- package/extension/doors/reviewWaveTools.ts +397 -0
- package/extension/doors/selfcheck.ts +1 -1
- package/extension/doors/submit.ts +82 -7
- package/extension/factories/gistAuthor.ts +4 -4
- package/extension/factories/gistDraft.ts +1 -1
- package/extension/factories/gistSave.ts +2 -2
- package/extension/factories/objective.ts +7 -7
- package/extension/factories/objectiveAuthor.ts +5 -4
- package/extension/factories/objectiveDraft.ts +153 -30
- package/extension/factories/objectiveDreamReport.ts +347 -0
- package/extension/factories/objectivePlan.ts +163 -9
- package/extension/factories/objectiveSave.ts +90 -2
- package/extension/factories/planMode.ts +5 -3
- package/extension/factories/planReview.ts +304 -46
- package/extension/factories/planTitle.ts +27 -9
- package/extension/hunkFeedback/inbox.ts +389 -0
- package/extension/hunkFeedback/perkFeedback.ts +364 -0
- package/extension/hunkFeedback/receiver.ts +296 -0
- package/extension/hunkFeedback/store.ts +533 -0
- package/extension/index.ts +169 -57
- package/extension/substrate/agentScratch.ts +171 -0
- package/extension/substrate/bindingDelivery.ts +9 -11
- package/extension/substrate/cache.ts +166 -23
- package/extension/substrate/coldDoor.ts +2 -3
- package/extension/substrate/command.ts +9 -6
- package/extension/substrate/config.ts +41 -12
- package/extension/substrate/git.ts +95 -2
- package/extension/substrate/providers.ts +4 -33
- package/extension/substrate/resources.ts +11 -0
- package/extension/substrate/result.ts +16 -7
- package/extension/substrate/sessionData.ts +8 -6
- package/extension/substrate/sessionPointers.ts +4 -5
- package/extension/substrate/structuredOutput.ts +78 -19
- package/extension/substrate/toolGating.ts +179 -23
- package/extension/substrate/workflowState.ts +50 -2
- package/extension/surfaces/footerProvider.ts +6 -6
- package/extension/surfaces/report.ts +38 -12
- package/extension/surfaces/surfaces.ts +213 -275
- package/extension/vendor/btw/btw.ts +94 -17
- package/extension/waves/adversarialReviewWave.ts +181 -0
- package/extension/waves/auditWave.ts +312 -0
- package/extension/waves/draftReviewWave.ts +205 -0
- package/extension/waves/dreamReducerWave.ts +700 -0
- package/extension/waves/dreamReport.ts +1494 -0
- package/extension/waves/dreamWave.ts +927 -0
- package/extension/waves/harvestWave.ts +399 -0
- package/extension/waves/memoryAdapter.ts +14 -1
- package/extension/waves/objectiveExplorerWave.ts +120 -0
- package/extension/waves/ponytail.ts +104 -0
- package/extension/waves/prReviewDynamicWave.ts +383 -72
- package/extension/waves/prReviewWave.ts +175 -25
- package/extension/waves/reportWave.ts +503 -107
- package/extension/waves/reviewClassifierWave.ts +109 -0
- package/extension/waves/rpcAdapter.ts +65 -2
- package/extension/worker/readOnlySession.ts +4 -5
- package/extension/worker/worker.ts +71 -93
- package/extension/workerMain.ts +4 -6
- package/package.json +18 -5
- package/prompts/README.md +19 -0
- package/prompts/_fixtures/live.yaml +115 -65
- package/prompts/commit-and-compact-continuation.md +13 -0
- package/prompts/common/resume-advisory.md +1 -0
- package/prompts/contexts/adapters/plannotator-gist.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +8 -3
- package/prompts/contexts/adapters/plannotator-plan.md +8 -3
- package/prompts/contexts/gist-authoring.md +6 -20
- package/prompts/contexts/objective-authoring.md +6 -18
- package/prompts/stages/address/action.md +3 -14
- package/prompts/stages/address/preview.md +2 -13
- package/prompts/stages/audit.md +18 -0
- package/prompts/stages/conflict-resolution.md +1 -1
- package/prompts/stages/gist-author/seed.md +4 -4
- package/prompts/stages/gist-save.md +1 -1
- package/prompts/stages/implement.md +3 -1
- package/prompts/stages/learn-code.md +1 -1
- package/prompts/stages/learn-docs.md +2 -2
- package/prompts/stages/learn-dream.md +10 -0
- package/prompts/stages/learn-harvest.md +15 -0
- package/prompts/stages/learn-orchestrate.md +1 -1
- package/prompts/stages/objective-author/adopt.md +5 -3
- package/prompts/stages/objective-author/file.md +5 -4
- package/prompts/stages/objective-author/seed.md +5 -4
- package/prompts/stages/objective-land.md +7 -0
- package/prompts/stages/objective-plan/guidance.md +2 -13
- package/prompts/stages/objective-plan/seed.md +5 -12
- package/prompts/stages/objective-recover.md +8 -0
- package/prompts/stages/objective-replan.md +4 -2
- package/prompts/stages/objective-review-browser.md +9 -0
- package/prompts/stages/objective-sync.md +6 -0
- package/prompts/stages/plan-from/adopt.md +2 -2
- package/prompts/stages/plan-from/file.md +2 -2
- package/prompts/stages/plan-review-browser.md +9 -0
- package/prompts/stages/pr-review-browser/active.md +6 -14
- package/prompts/stages/pr-review-browser/foreign.md +6 -14
- package/prompts/stages/pr-review-dynamic.md +6 -6
- package/prompts/stages/pr-review-terminal/active.md +7 -15
- package/prompts/stages/pr-review-terminal/foreign.md +7 -15
- package/prompts/stages/pr-review-terminal/local.md +1 -1
- package/prompts/stages/pr-review.md +5 -5
- package/prompts/stages/replan.md +3 -3
- package/shared/README.md +2 -2
- package/shared/bindings.yaml +15 -0
- package/shared/contracts-history.md +11 -0
- package/shared/contracts.md +5373 -281
- package/shared/providers.yaml +27 -60
- package/shared/registry.yaml +42 -18
- package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
- package/shared/schemas/outputs/objective-doctor.schema.json +487 -0
- package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
- package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
- package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
- package/shared/schemas/outputs/plan-save.schema.json +12 -0
- package/shared/schemas/outputs/pr-land.schema.json +3 -3
- package/shared/schemas/outputs/pr-submit.schema.json +177 -1
- package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
- package/extension/checkpoints/checkpoints.ts +0 -550
- package/extension/checkpoints/planSteps.ts +0 -108
- package/extension/doors/askUser.ts +0 -198
- package/prompts/common/output-schemas/objective-explorer.md +0 -36
- package/prompts/common/output-schemas/review-classifier.md +0 -47
- package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
stringField,
|
|
23
23
|
} from "../substrate/coldDoor.ts";
|
|
24
24
|
import { registerPerkCommand } from "../substrate/command.ts";
|
|
25
|
-
import {
|
|
25
|
+
import { subagentModel } from "../substrate/config.ts";
|
|
26
26
|
import { render } from "../substrate/prompts.ts";
|
|
27
27
|
import { failFor, type OkDetails, ok, type Result } from "../substrate/result.ts";
|
|
28
28
|
import { captureSessionPointer } from "../substrate/sessionPointers.ts";
|
|
@@ -44,6 +44,18 @@ export interface SubmitOk {
|
|
|
44
44
|
mergeable?: boolean | null;
|
|
45
45
|
/** The conflicted paths when `mergeable === false`; `[]` otherwise (advisory). */
|
|
46
46
|
conflicts?: string[];
|
|
47
|
+
/** `"stacked"` when the submit routed through the delivery publish operation (§8.47). */
|
|
48
|
+
delivery?: string;
|
|
49
|
+
/** The native-stack facts of a stacked submit (absent for the bottom layer). */
|
|
50
|
+
stack?: { number: number; size: number; position: number };
|
|
51
|
+
/** Lenient summary of an automatic suffix synchronization. */
|
|
52
|
+
operation?: {
|
|
53
|
+
kind: string;
|
|
54
|
+
operation_id: string | null;
|
|
55
|
+
no_op: boolean;
|
|
56
|
+
affected_count: number;
|
|
57
|
+
notes: string[];
|
|
58
|
+
};
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
export type SubmitResult = Result<SubmitOk>;
|
|
@@ -67,6 +79,51 @@ function conflictsField(payload: ColdJson): string[] {
|
|
|
67
79
|
return [];
|
|
68
80
|
}
|
|
69
81
|
|
|
82
|
+
/**
|
|
83
|
+
* A lenient read of the advisory stacked `stack` facts: all three numbers or nothing — a
|
|
84
|
+
* malformed value must NOT sink a successful submit decode (it just drops the suffix).
|
|
85
|
+
*/
|
|
86
|
+
function stackField(payload: ColdJson): SubmitOk["stack"] {
|
|
87
|
+
const value = objectField(payload, "stack");
|
|
88
|
+
if (value === undefined) return undefined;
|
|
89
|
+
const number = numberField(value, "number");
|
|
90
|
+
const size = numberField(value, "size");
|
|
91
|
+
const position = numberField(value, "position");
|
|
92
|
+
if (number === undefined || size === undefined || position === undefined) return undefined;
|
|
93
|
+
return { number, size, position };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Lenient all-or-nothing decode of the cascade operation block. The full affected rows remain a
|
|
98
|
+
* Python/CLI detail; the warm door needs only their count plus the recovery notes.
|
|
99
|
+
*/
|
|
100
|
+
function operationField(payload: ColdJson): SubmitOk["operation"] {
|
|
101
|
+
const value = objectField(payload, "operation");
|
|
102
|
+
if (value === undefined) return undefined;
|
|
103
|
+
const kind = stringField(value, "kind");
|
|
104
|
+
const operationId = value.operation_id;
|
|
105
|
+
const noOp = booleanField(value, "no_op");
|
|
106
|
+
const affected = value.affected;
|
|
107
|
+
const notes = value.notes;
|
|
108
|
+
if (
|
|
109
|
+
kind === undefined ||
|
|
110
|
+
(typeof operationId !== "string" && operationId !== null) ||
|
|
111
|
+
noOp === undefined ||
|
|
112
|
+
!Array.isArray(affected) ||
|
|
113
|
+
!Array.isArray(notes) ||
|
|
114
|
+
!notes.every((note) => typeof note === "string")
|
|
115
|
+
) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
kind,
|
|
120
|
+
operation_id: operationId,
|
|
121
|
+
no_op: noOp,
|
|
122
|
+
affected_count: affected.length,
|
|
123
|
+
notes: notes as string[],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
70
127
|
/**
|
|
71
128
|
* Narrow the `perk pr submit --json` success payload; strict on `pr`, lenient on the rest. The
|
|
72
129
|
* `base`/`mergeable`/`conflicts` mergeability fields are advisory (mirror land.ts's lenient
|
|
@@ -90,12 +147,15 @@ function decodeSubmit(payload: ColdJson): SubmitOk | null {
|
|
|
90
147
|
base: stringField(payload, "base"),
|
|
91
148
|
mergeable: mergeableField(payload),
|
|
92
149
|
conflicts: conflictsField(payload),
|
|
150
|
+
delivery: stringField(payload, "delivery"),
|
|
151
|
+
stack: stackField(payload),
|
|
152
|
+
operation: operationField(payload),
|
|
93
153
|
};
|
|
94
154
|
}
|
|
95
155
|
|
|
96
|
-
/** A definitively-unmergeable submit
|
|
156
|
+
/** A definitively-unmergeable submit; parsed conflict paths are advisory only. */
|
|
97
157
|
function isUnmergeable(details: SubmitDetails): details is OkDetails<SubmitOk> {
|
|
98
|
-
return details.ok && details.mergeable === false
|
|
158
|
+
return details.ok && details.mergeable === false;
|
|
99
159
|
}
|
|
100
160
|
|
|
101
161
|
/**
|
|
@@ -140,15 +200,30 @@ export async function submitPr(pi: ExtensionAPI, ctx: ExtensionContext): Promise
|
|
|
140
200
|
}
|
|
141
201
|
|
|
142
202
|
const verb = r.data.pr.existed ? "Found existing" : "Opened draft";
|
|
143
|
-
const conflicted = r.data.mergeable === false
|
|
203
|
+
const conflicted = r.data.mergeable === false;
|
|
144
204
|
// Reset the counter on every clean (or undetermined) submit — idempotent; keeps a later
|
|
145
205
|
// independent conflict bounded fresh.
|
|
146
206
|
if (r.data.mergeable !== false) resetConflictAttempts(pi, ctx);
|
|
207
|
+
// Automatic-cascade facts supersede the generic stacked suffix. A malformed operation block was
|
|
208
|
+
// dropped by the lenient decoder, so it falls back to the pre-existing stack wording.
|
|
209
|
+
const deliverySuffix =
|
|
210
|
+
r.data.operation?.kind === "sync"
|
|
211
|
+
? r.data.operation.no_op
|
|
212
|
+
? " (suffix already in sync)"
|
|
213
|
+
: ` (cascaded ${r.data.operation.affected_count} layer(s))`
|
|
214
|
+
: r.data.stack
|
|
215
|
+
? ` (stack #${r.data.stack.number}, layer ${r.data.stack.position}/${r.data.stack.size})`
|
|
216
|
+
: r.data.delivery === "stacked"
|
|
217
|
+
? " (stacked layer)"
|
|
218
|
+
: "";
|
|
219
|
+
for (const note of r.data.operation?.notes ?? []) {
|
|
220
|
+
report(ctx, "submit", "warning", note);
|
|
221
|
+
}
|
|
147
222
|
const message = conflicted
|
|
148
|
-
? `${verb} PR #${r.data.pr.number} → ${r.data.pr.url} — merge conflicts detected; resolving`
|
|
223
|
+
? `${verb} PR #${r.data.pr.number} → ${r.data.pr.url} — merge conflicts detected; resolving${deliverySuffix}`
|
|
149
224
|
: `${verb} PR #${r.data.pr.number} → ${r.data.pr.url} (${
|
|
150
225
|
r.data.plan_embedded ? "plan embedded" : "no plan embed"
|
|
151
|
-
})`;
|
|
226
|
+
})${deliverySuffix}`;
|
|
152
227
|
return ok(message, r.data, { terminate: true });
|
|
153
228
|
}
|
|
154
229
|
|
|
@@ -221,7 +296,7 @@ export function driveConflictResolution(
|
|
|
221
296
|
scope: "submit",
|
|
222
297
|
failure: `conflict_resolution_attempts read-back failed (expected ${next})`,
|
|
223
298
|
});
|
|
224
|
-
const model =
|
|
299
|
+
const model = subagentModel(ctx.cwd, "conflict-resolver");
|
|
225
300
|
const message =
|
|
226
301
|
conflictResolutionGuidance(base, next, CONFLICT_RESOLUTION_ATTEMPT_CAP, model) +
|
|
227
302
|
bindingSuffix(ctx.cwd, "command:submit");
|
|
@@ -28,10 +28,10 @@ export const GIST_AUTHOR_CONTEXT_TYPE = "perk:gist-author-context";
|
|
|
28
28
|
const GIST_AUTHOR_MARKER = "[GIST AUTHORING]";
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* The
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* The gist-authoring session context: live state + pointers only (contracts.md §8.57 — the flow
|
|
32
|
+
* is stated by the launch statement, the detail by the `perk-gist-author` skill). It names the
|
|
33
|
+
* working-draft artifact (`gist_draft`), the review tool (`plan_review`), and the bound skill;
|
|
34
|
+
* it never restates the flow. Prompting, NOT enforcement (the tool gate is the enforcement).
|
|
35
35
|
*/
|
|
36
36
|
export const GIST_AUTHORING_CONTEXT = render("contexts/gist-authoring.md", {
|
|
37
37
|
marker: GIST_AUTHOR_MARKER,
|
|
@@ -232,7 +232,7 @@ export function registerGistDraft(pi: ExtensionAPI): void {
|
|
|
232
232
|
type: "string",
|
|
233
233
|
description:
|
|
234
234
|
"The gist prose (the problem-space intent: what we want, why it matters, what " +
|
|
235
|
-
"bounds it — no implementation steps).",
|
|
235
|
+
"bounds it, and any high-level solution leanings — no implementation steps).",
|
|
236
236
|
},
|
|
237
237
|
title: {
|
|
238
238
|
type: "string",
|
|
@@ -155,7 +155,7 @@ export async function gistApprovalSave(
|
|
|
155
155
|
|
|
156
156
|
const TOOL_GUIDELINES = [
|
|
157
157
|
"Use gist_save only after the gist says what it means; it creates the tracked gist in the issue backend and ends the turn.",
|
|
158
|
-
"Pass gist_save the statement-of-intent PROSE in `prose` — problem-
|
|
158
|
+
"Pass gist_save the statement-of-intent PROSE in `prose` — problem-focused, with at most high-level solution leanings; no implementation steps or roadmap.",
|
|
159
159
|
"Pass gist_save's `scope` only once the consumption tier is settled (plan or objective); omit it to keep the pre-seeded/default scope.",
|
|
160
160
|
];
|
|
161
161
|
|
|
@@ -190,7 +190,7 @@ export function registerGistSave(pi: ExtensionAPI, gating: ToolGating): void {
|
|
|
190
190
|
type: "string",
|
|
191
191
|
description:
|
|
192
192
|
"The gist prose (the problem-space intent: what we want, why it matters, what " +
|
|
193
|
-
"bounds it — no implementation steps).",
|
|
193
|
+
"bounds it, and any high-level solution leanings — no implementation steps).",
|
|
194
194
|
},
|
|
195
195
|
title: {
|
|
196
196
|
type: "string",
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// the "fire only when…" tools live elsewhere. This module ships three pieces, all inert when no
|
|
8
8
|
// objective is active:
|
|
9
9
|
// 1. `/objective [<id>|clear]` — set/clear `active_objective` (LWW field) + seed a dedicated
|
|
10
|
-
// `perk:objective-budget` activation marker (high-churn budget data kept OFF the shared
|
|
11
|
-
//
|
|
10
|
+
// `perk:objective-budget` activation marker (high-churn budget data kept OFF the shared
|
|
11
|
+
// record).
|
|
12
12
|
// 2. Budget accounting — stateless rebuild (the goal.ts pattern): sum assistant-message tokens
|
|
13
13
|
// AFTER the latest activation marker; surface via ctx.ui guarded by ctx.hasUI; rebuilt on
|
|
14
14
|
// session_start AND session_tree AND agent_settled (survives reload/branch/compaction for
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// objective is active, compact when context usage crosses a configurable threshold.
|
|
20
20
|
//
|
|
21
21
|
// Headless-fail-safe: every UI call is ctx.hasUI-guarded; budget accounting + compaction are
|
|
22
|
-
// best-effort and never throw (logged-not-thrown
|
|
22
|
+
// best-effort and never throw (logged-not-thrown).
|
|
23
23
|
|
|
24
24
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
25
25
|
import { registerPerkCommand } from "../substrate/command.ts";
|
|
@@ -131,8 +131,8 @@ function activeObjective(ctx: ExtensionContext): string | null {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* Surface the budget as the
|
|
135
|
-
* widget is retired; the
|
|
134
|
+
* Surface the budget as the single-value `perk` status (the `perk-objective`
|
|
135
|
+
* widget is retired; the value carries id + tokens + elapsed). Headless-safe:
|
|
136
136
|
* the handle no-ops without UI.
|
|
137
137
|
*/
|
|
138
138
|
function renderStatus(ctx: ExtensionContext, status: PerkStatusHandle): void {
|
|
@@ -140,11 +140,11 @@ function renderStatus(ctx: ExtensionContext, status: PerkStatusHandle): void {
|
|
|
140
140
|
try {
|
|
141
141
|
const active = activeObjective(ctx);
|
|
142
142
|
if (active === null) {
|
|
143
|
-
status.set(ctx,
|
|
143
|
+
status.set(ctx, undefined);
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
const budget = rebuildBudget(scanBranchOf(ctx), Date.now());
|
|
147
|
-
status.set(ctx,
|
|
147
|
+
status.set(ctx, `${MARK_OBJECTIVE} ${active} · ${formatBudgetLine(budget)}`);
|
|
148
148
|
} catch (error) {
|
|
149
149
|
console.error(`perk: objective status render failed — ${error}`);
|
|
150
150
|
}
|
|
@@ -29,10 +29,11 @@ export const OBJECTIVE_AUTHOR_CONTEXT_TYPE = "perk:objective-author-context";
|
|
|
29
29
|
const OBJECTIVE_AUTHOR_MARKER = "[OBJECTIVE AUTHORING]";
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* The objective-authoring session context: live state + pointers only (contracts.md §8.57 — the
|
|
33
|
+
* flow is stated by the launch statement, the detail by the `perk-objective-author` skill). It
|
|
34
|
+
* names the working-draft artifact (`objective_draft`), the review tool (`plan_review`), and
|
|
35
|
+
* the bound skill; it never restates the flow. Prompting, NOT enforcement (the tool gate is the
|
|
36
|
+
* enforcement).
|
|
36
37
|
*/
|
|
37
38
|
export const OBJECTIVE_AUTHORING_CONTEXT = render("contexts/objective-authoring.md", {
|
|
38
39
|
marker: OBJECTIVE_AUTHOR_MARKER,
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
// `objective_save`/`/objective-save` still persist the objective to GitHub.
|
|
12
12
|
//
|
|
13
13
|
// Format doctrine: JSON is the storage/transport format, NEVER the human review surface. The
|
|
14
|
-
// artifact carries `{schema_version, title?, prose, roadmap}`
|
|
15
|
-
//
|
|
14
|
+
// artifact carries `{schema_version, title?, prose, roadmap}` (plus, in a perk learn dream
|
|
15
|
+
// session, the tool-written `dream_report` block — contracts §8.63) — the structured roadmap
|
|
16
|
+
// rides verbatim (node-shape validation stays with the Python plane at save time, the
|
|
16
17
|
// `parse_structured_roadmap` path). The review surface reads the draft via
|
|
17
18
|
// `readObjectiveDraft` (over `readSessionArtifact` — digest-validated, fail-open) and renders
|
|
18
19
|
// markdown via `renderObjectiveDraft` (the prose + a roadmap table) — never raw JSON; the
|
|
@@ -20,9 +21,10 @@
|
|
|
20
21
|
// back as structured JSON.
|
|
21
22
|
//
|
|
22
23
|
// Vocabulary ownership: this module owns the shared draft/save param vocabulary
|
|
23
|
-
// (`ObjectiveSaveParams`, `decodeObjectiveSaveParams`, `ROADMAP_PARAM_SCHEMA
|
|
24
|
-
// the LEAF (mirroring planDraft←planSave's
|
|
25
|
-
// value-import `readObjectiveDraft`
|
|
24
|
+
// (`ObjectiveSaveParams`, `decodeObjectiveSaveParams`, `ROADMAP_PARAM_SCHEMA`,
|
|
25
|
+
// `DREAM_REPORT_PARAM_SCHEMA`) — objectiveDraft is the LEAF (mirroring planDraft←planSave's
|
|
26
|
+
// direction); objectiveSave.ts consumes it, so it may value-import `readObjectiveDraft`
|
|
27
|
+
// cycle-free for the approval→save orchestration.
|
|
26
28
|
//
|
|
27
29
|
// Imports stay node builtins + sibling seams (sessionData.ts, result.ts) so the module loads
|
|
28
30
|
// under `node --test`; no manual `scratch`/`runs` path segments (cacheGuard.test.ts).
|
|
@@ -37,9 +39,18 @@ import {
|
|
|
37
39
|
type SessionDataCtx,
|
|
38
40
|
writeSessionArtifact,
|
|
39
41
|
} from "../substrate/sessionData.ts";
|
|
40
|
-
import { arrayParam, paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
42
|
+
import { arrayParam, objectParam, paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
41
43
|
import type { EntrySink } from "../substrate/workflowState.ts";
|
|
42
44
|
import type { ReportTarget } from "../surfaces/report.ts";
|
|
45
|
+
import { DREAM_REPORT_INPUT_SCHEMA } from "../waves/dreamReport.ts";
|
|
46
|
+
import {
|
|
47
|
+
decodeDreamReportBlock,
|
|
48
|
+
type ObjectiveDreamReportBlock,
|
|
49
|
+
resolveDreamReportGate,
|
|
50
|
+
} from "./objectiveDreamReport.ts";
|
|
51
|
+
|
|
52
|
+
/** The reviewed objective delivery choice (contracts §8.45). */
|
|
53
|
+
export type DeliveryChoice = "incremental" | "stacked";
|
|
43
54
|
|
|
44
55
|
/** The decoded `objective_save` tool params (shared with `objective_draft`). */
|
|
45
56
|
export interface ObjectiveSaveParams {
|
|
@@ -48,8 +59,41 @@ export interface ObjectiveSaveParams {
|
|
|
48
59
|
roadmap?: unknown[];
|
|
49
60
|
// The objective's target branch; omitted to use the repo default.
|
|
50
61
|
base?: string;
|
|
62
|
+
// The reviewed delivery choice; omitted ⇒ incremental (the §8.42 absence rule).
|
|
63
|
+
delivery?: DeliveryChoice;
|
|
64
|
+
// The dream-report input (perk learn dream only — §8.63); deep validation is the gate
|
|
65
|
+
// resolver's, so the decode keeps it opaque beyond the plain-object shape.
|
|
66
|
+
dream_report?: unknown;
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The `delivery` enum property, shared between `objective_save` and `objective_draft` so the
|
|
71
|
+
* two tools' delivery contracts cannot drift. The description bakes in the explicit-human-choice
|
|
72
|
+
* discipline: the agent must ASK, with incremental recommended.
|
|
73
|
+
*/
|
|
74
|
+
export const DELIVERY_PARAM_SCHEMA = {
|
|
75
|
+
type: "string",
|
|
76
|
+
enum: ["incremental", "stacked"],
|
|
77
|
+
description:
|
|
78
|
+
"The reviewed delivery choice — ask the human explicitly (incremental is the recommended " +
|
|
79
|
+
"default: each plan lands independently; stacked lands ALL non-skipped roadmap nodes as " +
|
|
80
|
+
"one atomic PR train — capability-checked at save).",
|
|
81
|
+
} as const;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The `dream_report` property, shared between `objective_save` and `objective_draft` so the
|
|
85
|
+
* two tools' dream contracts cannot drift: the §8.62 `DREAM_REPORT_INPUT_SCHEMA` embedded by
|
|
86
|
+
* identifier (the `DELIVERY_PARAM_SCHEMA`/`ROADMAP_PARAM_SCHEMA` shared-schema pattern) plus
|
|
87
|
+
* the gate description. Structurally reachable only inside a `perk learn dream` session (the
|
|
88
|
+
* resolver refuses it outside one).
|
|
89
|
+
*/
|
|
90
|
+
export const DREAM_REPORT_PARAM_SCHEMA = {
|
|
91
|
+
...DREAM_REPORT_INPUT_SCHEMA,
|
|
92
|
+
description:
|
|
93
|
+
"The perk learn dream session's final report input (the parent's decisions only) — " +
|
|
94
|
+
"required inside a dream session, refused outside one.",
|
|
95
|
+
} as const;
|
|
96
|
+
|
|
53
97
|
/**
|
|
54
98
|
* The roadmap-node items JSON schema, shared between `objective_save` and `objective_draft`
|
|
55
99
|
* so the two tools' roadmap contracts cannot drift.
|
|
@@ -96,8 +140,23 @@ export function decodeObjectiveSaveParams(params: unknown): ObjectiveSaveParams
|
|
|
96
140
|
const title = stringParam(p, "title");
|
|
97
141
|
const roadmap = arrayParam(p, "roadmap");
|
|
98
142
|
const base = stringParam(p, "base");
|
|
99
|
-
|
|
100
|
-
|
|
143
|
+
const delivery = stringParam(p, "delivery");
|
|
144
|
+
// `dream_report` must be a plain object when present (absent → undefined); deep validation
|
|
145
|
+
// stays with the gate resolver (resolveDreamReportGate).
|
|
146
|
+
const dreamReport = objectParam(p, "dream_report");
|
|
147
|
+
if (
|
|
148
|
+
prose === null ||
|
|
149
|
+
title === null ||
|
|
150
|
+
roadmap === null ||
|
|
151
|
+
base === null ||
|
|
152
|
+
delivery === null ||
|
|
153
|
+
dreamReport === null
|
|
154
|
+
) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
// The delivery enum is strict beyond `string`: an off-enum value is present-but-mistyped.
|
|
158
|
+
if (delivery !== undefined && delivery !== "incremental" && delivery !== "stacked") return null;
|
|
159
|
+
return { prose: prose ?? "", title, roadmap, base, delivery, dream_report: dreamReport };
|
|
101
160
|
}
|
|
102
161
|
|
|
103
162
|
/** The fixed working-objective artifact name (one JSON file: prose + the structured roadmap). */
|
|
@@ -120,12 +179,21 @@ export type ObjectiveDraftResult = Result<ObjectiveDraftOk>;
|
|
|
120
179
|
* structured roadmap, verbatim — the draft never validates node shapes) as one JSON artifact and
|
|
121
180
|
* write it through the accessor seam (file + `session_artifacts` provenance pointer). Soft
|
|
122
181
|
* result, never throws — failure taxonomy: empty prose → `invalid_input`; no session run_id →
|
|
123
|
-
* `no_run_id`;
|
|
182
|
+
* `no_run_id`; a `dream_report` gate refusal → `invalid_input`/`bad_state` (the §8.63 matrix —
|
|
183
|
+
* validated at write time so a report-less dream bundle can never reach review);
|
|
184
|
+
* file-or-pointer write failure → `write_failed` (the seam already warned).
|
|
124
185
|
*/
|
|
125
186
|
export function writeObjectiveDraft(
|
|
126
187
|
sink: EntrySink,
|
|
127
188
|
ctx: SessionDataCtx & ReportTarget,
|
|
128
|
-
opts: {
|
|
189
|
+
opts: {
|
|
190
|
+
prose: string;
|
|
191
|
+
title?: string;
|
|
192
|
+
roadmap?: unknown[];
|
|
193
|
+
base?: string;
|
|
194
|
+
delivery?: DeliveryChoice;
|
|
195
|
+
dream_report?: unknown;
|
|
196
|
+
},
|
|
129
197
|
): ObjectiveDraftResult {
|
|
130
198
|
const fail = failFor(ctx, "objective-draft");
|
|
131
199
|
|
|
@@ -138,14 +206,25 @@ export function writeObjectiveDraft(
|
|
|
138
206
|
return fail("session has no run_id — cannot write the objective-draft artifact", "no_run_id");
|
|
139
207
|
}
|
|
140
208
|
|
|
141
|
-
//
|
|
209
|
+
// The §8.63 gate: validated at draft-write time via buildDreamReport, the ONE stamp stored
|
|
210
|
+
// with the block; `absent` keeps the payload byte-identical (every non-dream path unchanged).
|
|
211
|
+
const gate = resolveDreamReportGate(ctx, opts.dream_report, new Date().toISOString());
|
|
212
|
+
if (gate.kind === "refuse") {
|
|
213
|
+
return fail(gate.detail, gate.errorType);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Deterministic key order via the explicit literal; `title`/`base`/`delivery` are omitted
|
|
217
|
+
// when blank/absent (schema_version stays 1 — an additive optional field, fail-open readers).
|
|
142
218
|
const title = opts.title?.trim();
|
|
143
219
|
const base = opts.base?.trim();
|
|
220
|
+
const delivery = opts.delivery;
|
|
144
221
|
const roadmap = opts.roadmap ?? [];
|
|
145
222
|
const payload = {
|
|
146
223
|
schema_version: 1,
|
|
147
224
|
...(title ? { title } : {}),
|
|
148
225
|
...(base ? { base } : {}),
|
|
226
|
+
...(delivery ? { delivery } : {}),
|
|
227
|
+
...(gate.kind === "block" ? { dream_report: gate.block } : {}),
|
|
149
228
|
prose: opts.prose,
|
|
150
229
|
roadmap,
|
|
151
230
|
};
|
|
@@ -181,6 +260,10 @@ export interface ObjectiveDraft {
|
|
|
181
260
|
roadmap: unknown[];
|
|
182
261
|
// The objective's target branch; kept only when a non-blank string in the artifact.
|
|
183
262
|
base?: string;
|
|
263
|
+
// The reviewed delivery choice; kept only when exactly the enum (junk → absent, like `base`).
|
|
264
|
+
delivery?: DeliveryChoice;
|
|
265
|
+
// The dream-report block (§8.63); a present-but-malformed block refuses the WHOLE draft.
|
|
266
|
+
dream_report?: ObjectiveDreamReportBlock;
|
|
184
267
|
}
|
|
185
268
|
|
|
186
269
|
/**
|
|
@@ -188,7 +271,10 @@ export interface ObjectiveDraft {
|
|
|
188
271
|
* `readSessionArtifact`'s loud tier): no pointer/file/digest → `null` (the seam already spoke);
|
|
189
272
|
* malformed JSON, a non-object payload, an unsupported `schema_version`, or blank prose → a
|
|
190
273
|
* stderr warning + `null`. `roadmap` defaults to `[]` when absent/non-array; `title` is kept
|
|
191
|
-
* only when a non-blank string.
|
|
274
|
+
* only when a non-blank string. A present-but-malformed `dream_report` block refuses the WHOLE
|
|
275
|
+
* draft (warn + `null`) — deliberately stricter than the lenient junk→absent handling of
|
|
276
|
+
* `base`/`delivery`, because silently dropping a malformed report is exactly what §8.63
|
|
277
|
+
* forbids. Never throws.
|
|
192
278
|
*/
|
|
193
279
|
export function readObjectiveDraft(ctx: SessionDataCtx): ObjectiveDraft | null {
|
|
194
280
|
const artifact = readSessionArtifact(ctx, OBJECTIVE_DRAFT_ARTIFACT);
|
|
@@ -219,9 +305,23 @@ export function readObjectiveDraft(ctx: SessionDataCtx): ObjectiveDraft | null {
|
|
|
219
305
|
const title =
|
|
220
306
|
typeof payload.title === "string" && payload.title.trim() ? payload.title : undefined;
|
|
221
307
|
const base = typeof payload.base === "string" && payload.base.trim() ? payload.base : undefined;
|
|
308
|
+
const delivery =
|
|
309
|
+
payload.delivery === "incremental" || payload.delivery === "stacked"
|
|
310
|
+
? payload.delivery
|
|
311
|
+
: undefined;
|
|
312
|
+
let dreamReport: ObjectiveDreamReportBlock | undefined;
|
|
313
|
+
if ("dream_report" in payload) {
|
|
314
|
+
const block = decodeDreamReportBlock(payload.dream_report);
|
|
315
|
+
if (block === null) {
|
|
316
|
+
return refuse("carries a malformed dream_report block");
|
|
317
|
+
}
|
|
318
|
+
dreamReport = block;
|
|
319
|
+
}
|
|
222
320
|
return {
|
|
223
321
|
...(title !== undefined ? { title } : {}),
|
|
224
322
|
...(base !== undefined ? { base } : {}),
|
|
323
|
+
...(delivery !== undefined ? { delivery } : {}),
|
|
324
|
+
...(dreamReport !== undefined ? { dream_report: dreamReport } : {}),
|
|
225
325
|
prose,
|
|
226
326
|
roadmap,
|
|
227
327
|
};
|
|
@@ -251,31 +351,52 @@ function nodeDependsOn(node: unknown): string {
|
|
|
251
351
|
/**
|
|
252
352
|
* Render the draft as the markdown review surface (JSON is storage/transport only — contracts
|
|
253
353
|
* §8.1): the optional `# title` heading, the prose verbatim, and (when the roadmap is non-empty)
|
|
254
|
-
* a `## Roadmap` section with ONE markdown table.
|
|
255
|
-
*
|
|
354
|
+
* a `## Roadmap` section with ONE markdown table. A prominent `**Delivery:**` line renders
|
|
355
|
+
* directly under the title unconditionally (the reviewed choice must be visible either way —
|
|
356
|
+
* contracts §8.45). The `Phase` column appears only when some node carries a non-blank string
|
|
357
|
+
* `phase`. When the draft carries a `dream_report` block, the stored CANONICAL parts append as
|
|
358
|
+
* the final section — the review surface IS the approval bundle: the objective and its report
|
|
359
|
+
* review (and are approved or denied) together (§8.63); the parts carry their own
|
|
360
|
+
* `# Dream report — <run_id>` headers. Pure; never throws.
|
|
256
361
|
*/
|
|
362
|
+
/** The always-present prominent `**Delivery:**` review line (contracts §8.45). */
|
|
363
|
+
function deliveryLine(draft: ObjectiveDraft): string {
|
|
364
|
+
if (draft.delivery === "stacked") {
|
|
365
|
+
return (
|
|
366
|
+
"**Delivery: STACKED** — all non-skipped roadmap nodes land as ONE atomic pull-request " +
|
|
367
|
+
"train (capability-checked at save)"
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return "**Delivery: incremental** (the default — each plan lands independently)";
|
|
371
|
+
}
|
|
372
|
+
|
|
257
373
|
export function renderObjectiveDraft(draft: ObjectiveDraft): string {
|
|
258
374
|
let out = "";
|
|
259
375
|
if (draft.title) out += `# ${draft.title}\n\n`;
|
|
376
|
+
out += `${deliveryLine(draft)}\n\n`;
|
|
260
377
|
out += draft.prose;
|
|
261
378
|
|
|
262
|
-
if (draft.roadmap.length
|
|
379
|
+
if (draft.roadmap.length > 0) {
|
|
380
|
+
const withPhase = draft.roadmap.some((node) => nodeString(node, "phase").trim().length > 0);
|
|
381
|
+
const header = withPhase
|
|
382
|
+
? "| Node | Phase | Description | Depends On | Status |\n| --- | --- | --- | --- | --- |"
|
|
383
|
+
: "| Node | Description | Depends On | Status |\n| --- | --- | --- | --- |";
|
|
384
|
+
const rows = draft.roadmap.map((node) => {
|
|
385
|
+
const cells = [
|
|
386
|
+
tableCell(nodeString(node, "id")),
|
|
387
|
+
...(withPhase ? [tableCell(nodeString(node, "phase"))] : []),
|
|
388
|
+
tableCell(nodeString(node, "description")),
|
|
389
|
+
tableCell(nodeDependsOn(node)),
|
|
390
|
+
tableCell(nodeString(node, "status") || "pending"),
|
|
391
|
+
];
|
|
392
|
+
return `| ${cells.join(" | ")} |`;
|
|
393
|
+
});
|
|
394
|
+
out = `${out.trimEnd()}\n\n## Roadmap\n\n${header}\n${rows.join("\n")}\n`;
|
|
395
|
+
}
|
|
263
396
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
: "| Node | Description | Depends On | Status |\n| --- | --- | --- | --- |";
|
|
268
|
-
const rows = draft.roadmap.map((node) => {
|
|
269
|
-
const cells = [
|
|
270
|
-
tableCell(nodeString(node, "id")),
|
|
271
|
-
...(withPhase ? [tableCell(nodeString(node, "phase"))] : []),
|
|
272
|
-
tableCell(nodeString(node, "description")),
|
|
273
|
-
tableCell(nodeDependsOn(node)),
|
|
274
|
-
tableCell(nodeString(node, "status") || "pending"),
|
|
275
|
-
];
|
|
276
|
-
return `| ${cells.join(" | ")} |`;
|
|
277
|
-
});
|
|
278
|
-
return `${out.trimEnd()}\n\n## Roadmap\n\n${header}\n${rows.join("\n")}\n`;
|
|
397
|
+
if (draft.dream_report === undefined) return out;
|
|
398
|
+
// The approval bundle: objective first, then the stored CANONICAL report parts.
|
|
399
|
+
return `${out.trimEnd()}\n\n${draft.dream_report.parts.join("\n\n")}\n`;
|
|
279
400
|
}
|
|
280
401
|
|
|
281
402
|
const TOOL_GUIDELINES = [
|
|
@@ -316,6 +437,8 @@ export function registerObjectiveDraft(pi: ExtensionAPI): void {
|
|
|
316
437
|
description:
|
|
317
438
|
"Optional target branch for this objective's plans (omit to use the repo default).",
|
|
318
439
|
},
|
|
440
|
+
delivery: DELIVERY_PARAM_SCHEMA,
|
|
441
|
+
dream_report: DREAM_REPORT_PARAM_SCHEMA,
|
|
319
442
|
roadmap: {
|
|
320
443
|
type: "array",
|
|
321
444
|
description:
|