@mgiles/perk 3.1.0 → 3.3.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 +5 -0
- package/extension/authoring/gist/draft.ts +198 -0
- package/extension/authoring/gist/prose.ts +46 -0
- package/extension/authoring/gist/review.ts +133 -0
- package/extension/authoring/gist/save.ts +118 -0
- package/extension/authoring/objective/draft.ts +345 -0
- package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
- package/extension/authoring/objective/planning.ts +124 -0
- package/extension/authoring/objective/prose.ts +103 -0
- package/extension/authoring/objective/review.ts +128 -0
- package/extension/authoring/objective/save.ts +224 -0
- package/extension/authoring/plan/draft.ts +84 -0
- package/extension/authoring/plan/prose.ts +41 -0
- package/extension/authoring/plan/review.ts +269 -0
- package/extension/authoring/plan/save.ts +256 -0
- package/extension/authoring/plan/source.ts +82 -0
- package/extension/authoring/refinement/context.ts +468 -0
- package/extension/authoring/refinement/draft.ts +261 -0
- package/extension/authoring/refinement/prose.ts +79 -0
- package/extension/authoring/refinement/review.ts +111 -0
- package/extension/authoring/refinement/save.ts +119 -0
- package/extension/authoring/review/approvalGate.ts +34 -0
- package/extension/authoring/review/draftContext.ts +68 -0
- package/extension/codeReview/automated.ts +352 -0
- package/extension/codeReview/submission.ts +229 -0
- package/extension/delivery/address.ts +295 -0
- package/extension/delivery/ci.ts +355 -0
- package/extension/delivery/commitCompact.ts +93 -0
- package/extension/delivery/conflictResolution.ts +247 -0
- package/extension/delivery/ready.ts +193 -0
- package/extension/delivery/stackConflict.ts +361 -0
- package/extension/delivery/stackObjective.ts +16 -0
- package/extension/delivery/stackReconcile.ts +165 -0
- package/extension/delivery/submit.ts +171 -0
- package/extension/index.ts +369 -359
- package/extension/learning/analystWave.ts +324 -0
- package/extension/learning/audit.ts +667 -0
- package/extension/learning/capture.ts +92 -0
- package/extension/learning/containment.ts +104 -0
- package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
- package/extension/learning/dreamAnalysis.ts +435 -0
- package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
- package/extension/{waves → learning}/dreamReport.ts +35 -31
- package/extension/learning/harvest.ts +491 -0
- package/extension/learning/prose.ts +66 -0
- package/extension/learning/routing.ts +79 -0
- package/extension/pi/v1/bashScanTimeout.ts +64 -0
- package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -310
- package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +125 -57
- package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
- package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +169 -116
- package/extension/pi/v1/codeReview/stack.ts +606 -0
- package/extension/pi/v1/codeReview/submit.ts +354 -0
- package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
- package/extension/pi/v1/contextEvidence.ts +80 -0
- package/extension/pi/v1/contextInjection.ts +207 -0
- package/extension/{doors → pi/v1/delivery}/address.ts +163 -265
- package/extension/pi/v1/delivery/ci.ts +570 -0
- package/extension/pi/v1/delivery/commitCompact.ts +201 -0
- package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
- package/extension/{doors → pi/v1/delivery}/land.ts +128 -60
- package/extension/pi/v1/delivery/ready.ts +322 -0
- package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
- package/extension/pi/v1/delivery/stackDrive.ts +120 -0
- package/extension/pi/v1/delivery/stackLand.ts +223 -0
- package/extension/pi/v1/delivery/stackRecover.ts +265 -0
- package/extension/pi/v1/delivery/stackStatus.ts +237 -0
- package/extension/pi/v1/delivery/stackSync.ts +658 -0
- package/extension/pi/v1/delivery/submit.ts +389 -0
- package/extension/pi/v1/delivery/submitConflict.ts +186 -0
- package/extension/pi/v1/draftReview.ts +431 -0
- package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
- package/extension/pi/v1/gist.ts +794 -0
- package/extension/pi/v1/learning/audit.ts +186 -0
- package/extension/pi/v1/learning/dream.ts +207 -0
- package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
- package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
- package/extension/pi/v1/learning/learn.ts +585 -0
- package/extension/{doors → pi/v1}/lifecycleGates.ts +14 -59
- package/extension/{factories → pi/v1}/objective.ts +53 -33
- package/extension/pi/v1/objectiveAuthoring.ts +672 -0
- package/extension/pi/v1/objectiveDreamGate.ts +160 -0
- package/extension/pi/v1/objectivePlanning.ts +776 -0
- package/extension/pi/v1/objectiveRefinement.ts +1320 -0
- package/extension/pi/v1/objectiveReview.ts +451 -0
- package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
- package/extension/pi/v1/plan.ts +812 -0
- package/extension/pi/v1/planReview.ts +820 -0
- package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
- package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
- package/extension/pi/v1/providers/plannotator.ts +487 -0
- package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +152 -34
- package/extension/pi/v1/providers/selection.ts +43 -0
- package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
- package/extension/pi/v1/review.ts +538 -0
- package/extension/pi/v1/reviewOutcome.ts +9 -0
- package/extension/pi/v1/scoutWave.ts +318 -0
- package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
- package/extension/session/branchWorkflowSession.ts +60 -0
- package/extension/session/lifecycle.ts +644 -0
- package/extension/session/lifecycleGates.ts +64 -0
- package/extension/session/saveDestination.ts +87 -0
- package/extension/session/workflowSession.ts +971 -0
- package/extension/substrate/agentScratch.ts +27 -54
- package/extension/substrate/bashScanTimeout.ts +181 -0
- package/extension/substrate/bindingDelivery.ts +38 -30
- package/extension/substrate/bindings.ts +4 -5
- package/extension/substrate/cache.ts +64 -12
- package/extension/substrate/childRestrictions.ts +39 -0
- package/extension/substrate/coldDoor.ts +17 -1
- package/extension/substrate/config.ts +157 -19
- package/extension/substrate/git.ts +88 -6
- package/extension/substrate/modelVisible.ts +53 -0
- package/extension/substrate/paths.ts +2 -7
- package/extension/substrate/prompts.ts +22 -0
- package/extension/substrate/registry.ts +2 -0
- package/extension/substrate/resolverLease.ts +364 -0
- package/extension/substrate/sessionData.ts +85 -152
- package/extension/substrate/toolGating.ts +279 -84
- package/extension/substrate/unifiedDiff.ts +1 -1
- package/extension/substrate/workflowState.ts +191 -166
- package/extension/substrate/worktreeResolverLock.ts +261 -0
- package/extension/surfaces/surfaces.ts +79 -27
- package/extension/waves/adversarialReviewWave.ts +103 -48
- package/extension/waves/blockedReports.ts +59 -0
- package/extension/waves/draftReviewWave.ts +42 -42
- package/extension/waves/laneIdentity.ts +77 -0
- package/extension/waves/objectiveExplorerWave.ts +24 -24
- package/extension/waves/prReviewWave.ts +89 -77
- package/extension/waves/reportWave.ts +438 -578
- package/extension/waves/reviewClassifierWave.ts +22 -22
- package/extension/waves/rpcAdapter.ts +100 -15
- package/extension/waves/scoutWave.ts +192 -0
- package/extension/waves/transport.ts +480 -0
- package/extension/worker/sdkAdapter.ts +494 -0
- package/extension/worker/stageExecution.ts +679 -0
- package/extension/workerMain.ts +18 -19
- package/package.json +6 -4
- package/prompts/_fixtures/live.yaml +98 -10
- package/prompts/contexts/adapters/plannotator-gist.md +6 -0
- package/prompts/contexts/adapters/plannotator-objective.md +6 -0
- package/prompts/contexts/adapters/plannotator-plan.md +8 -1
- package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
- package/prompts/contexts/adapters/tombell-plan.md +4 -0
- package/prompts/contexts/objective-refinement.md +17 -0
- package/prompts/contexts/plan-authoring.md +6 -5
- package/prompts/contexts/read-only.md +1 -1
- package/prompts/stages/conflict-resolution-continuation.md +9 -0
- package/prompts/stages/conflict-resolution.md +4 -4
- package/prompts/stages/objective-author/adopt.md +1 -1
- package/prompts/stages/objective-author/file.md +1 -1
- package/prompts/stages/objective-author/seed.md +1 -1
- package/prompts/stages/objective-plan/guidance.md +2 -2
- package/prompts/stages/objective-plan/seed.md +9 -1
- package/prompts/stages/objective-reconcile-ready.md +7 -0
- package/prompts/stages/objective-reconcile.md +1 -1
- package/prompts/stages/objective-refine/seed.md +18 -0
- package/prompts/stages/objective-review-browser.md +4 -4
- package/prompts/stages/objective-sync.md +1 -1
- package/prompts/stages/plan-review-browser.md +4 -4
- package/prompts/stages/pr-review-browser/active.md +3 -4
- package/prompts/stages/pr-review-browser/foreign.md +3 -4
- package/prompts/stages/pr-review-terminal/active.md +3 -3
- package/prompts/stages/pr-review-terminal/foreign.md +3 -3
- package/prompts/stages/pr-review.md +3 -3
- package/prompts/stages/stack-review/cold.md +1 -0
- package/prompts/stages/stack-review-browser/stack.md +22 -0
- package/shared/README.md +8 -3
- package/shared/bindings.yaml +6 -3
- package/shared/contracts.md +4499 -2147
- package/shared/fixtures/issues-table.json +130 -0
- package/shared/registry.yaml +29 -1
- package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +178 -2
- package/shared/schemas/outputs/pr-ready.schema.json +110 -2
- package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
- package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
- package/extension/adapters/planAdapterPlannotator.ts +0 -362
- package/extension/doors/auditWaveTools.ts +0 -352
- package/extension/doors/ciExecutor.ts +0 -756
- package/extension/doors/commitCompact.ts +0 -251
- package/extension/doors/dreamWaveTools.ts +0 -475
- package/extension/doors/learn.ts +0 -655
- package/extension/doors/objectiveStack.ts +0 -1143
- package/extension/doors/prReviewDynamic.ts +0 -276
- package/extension/doors/ready.ts +0 -87
- package/extension/doors/submit.ts +0 -347
- package/extension/doors/submitPrReview.ts +0 -408
- package/extension/factories/gistAuthor.ts +0 -94
- package/extension/factories/gistDraft.ts +0 -265
- package/extension/factories/gistSave.ts +0 -251
- package/extension/factories/implementHere.ts +0 -116
- package/extension/factories/objectiveAuthor.ts +0 -98
- package/extension/factories/objectiveDraft.ts +0 -466
- package/extension/factories/objectivePlan.ts +0 -975
- package/extension/factories/objectiveSave.ts +0 -363
- package/extension/factories/planDraft.ts +0 -140
- package/extension/factories/planMode.ts +0 -205
- package/extension/factories/planReview.ts +0 -1237
- package/extension/factories/planSave.ts +0 -604
- package/extension/factories/planTitle.ts +0 -141
- package/extension/substrate/structuredOutput.ts +0 -202
- package/extension/waves/auditWave.ts +0 -312
- package/extension/waves/harvestWave.ts +0 -399
- package/extension/waves/learnWave.ts +0 -155
- package/extension/waves/memoryAdapter.ts +0 -139
- package/extension/waves/prReviewDynamicWave.ts +0 -777
- package/extension/worker/readOnlySession.ts +0 -294
- package/extension/worker/worker.ts +0 -899
- package/prompts/stages/pr-review-dynamic.md +0 -7
- package/shared/contracts-history.md +0 -605
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// The objective planning feature: the roadmap-node → plan-generation policy — the bounded
|
|
2
|
+
// node-transition operation (with the completion-audit gate and the warm node-claim carrier
|
|
3
|
+
// maintenance). The reconcile/add-node writes carry no feature policy beyond their decoders,
|
|
4
|
+
// so they live adapter-tier in pi/v1/objectivePlanning.ts (direct cold-door calls — no port).
|
|
5
|
+
//
|
|
6
|
+
// The completion-audit gate is a property of the MODEL-FACING boundary only — NOT an invariant
|
|
7
|
+
// on the node-`done` state: the canonical `perk objective node --status done` (human/CI cold
|
|
8
|
+
// CLI) has no audit gate, and the auto-on-merge node-done deliberately sets `done` without one.
|
|
9
|
+
// Both are intentional non-audited paths; the structural refusal protects the model's path
|
|
10
|
+
// only. The "are we done?" judgment text lives in the perk-objective-plan skill.
|
|
11
|
+
//
|
|
12
|
+
// Decode-once-at-the-edge: inputs arrive TYPED (the pi/v1 decoders own the tool-boundary
|
|
13
|
+
// shape), so the ops re-validate nothing. Canonical mutations stay in the Python plane — the
|
|
14
|
+
// backends delegate; the objective's canonical state is the issue (re-read on demand), and the
|
|
15
|
+
// only session state these ops maintain is the warm `objective_node_claim` carrier, THROUGH the
|
|
16
|
+
// session seam.
|
|
17
|
+
|
|
18
|
+
import type { WorkflowChangeResult, WorkflowSession } from "../../session/workflowSession.ts";
|
|
19
|
+
|
|
20
|
+
/** The valid node statuses (mirrors the Python `objective.NodeStatus` StrEnum). */
|
|
21
|
+
export const NODE_STATUSES = [
|
|
22
|
+
"pending",
|
|
23
|
+
"planning",
|
|
24
|
+
"in_progress",
|
|
25
|
+
"done",
|
|
26
|
+
"blocked",
|
|
27
|
+
"skipped",
|
|
28
|
+
] as const;
|
|
29
|
+
export type NodeStatus = (typeof NODE_STATUSES)[number];
|
|
30
|
+
|
|
31
|
+
/** The minimum trimmed length of a non-trivial completion `audit` (the pinnable predicate). */
|
|
32
|
+
export const MIN_AUDIT_LENGTH = 40;
|
|
33
|
+
|
|
34
|
+
/** A non-trivial audit iff it is a string whose value after `.trim()` is ≥ MIN_AUDIT_LENGTH. */
|
|
35
|
+
export function isNonTrivialAudit(audit: unknown): boolean {
|
|
36
|
+
return typeof audit === "string" && audit.trim().length >= MIN_AUDIT_LENGTH;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The typed `objective_node` input (`objective` is the opaque §8.21 string id). */
|
|
40
|
+
export interface ObjectiveNodeInput {
|
|
41
|
+
objective: string;
|
|
42
|
+
node: string;
|
|
43
|
+
status?: NodeStatus;
|
|
44
|
+
pr?: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
audit?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The narrow exterior port the node transition writes through (the cold door in pi/v1). */
|
|
50
|
+
export interface ObjectiveNodeBackend {
|
|
51
|
+
transition(req: {
|
|
52
|
+
objective: string;
|
|
53
|
+
node: string;
|
|
54
|
+
status?: NodeStatus;
|
|
55
|
+
pr?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
}): Promise<
|
|
58
|
+
| { status: "ok"; commentUpdated: boolean }
|
|
59
|
+
| { status: "failed"; message: string; errorType: string }
|
|
60
|
+
>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The transition outcome. `claimChange` is the session seam's own `WorkflowChangeResult`
|
|
65
|
+
* VERBATIM (`null` = not attempted: a pr/description-only call leaves the carrier untouched);
|
|
66
|
+
* the adapter's rendering ignores it (the seam's report() stays the loudness channel — a failed
|
|
67
|
+
* claim append never fails the tool result).
|
|
68
|
+
*/
|
|
69
|
+
export type TransitionObjectiveNodeOutcome =
|
|
70
|
+
| { status: "ok"; commentUpdated: boolean; claimChange: WorkflowChangeResult | null }
|
|
71
|
+
| { status: "failed"; message: string; errorType: string };
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The bounded `objective_node` transition: the completion-audit gate (`status:"done"` requires
|
|
75
|
+
* a non-trivial `audit` — model-path-only) → the no-change refusal (neither status nor pr nor
|
|
76
|
+
* description) → `backend.transition` → on success, maintain the warm node-link carrier
|
|
77
|
+
* THROUGH THE SEAM: `planning` records the claim (the exact moment the warm factory learns the
|
|
78
|
+
* node id; an idempotent re-claim short-circuits `unchanged`); any other explicit status clears
|
|
79
|
+
* it (the seam's both-field match — an unrelated claim is never clobbered); no status change
|
|
80
|
+
* leaves it untouched. Never throws.
|
|
81
|
+
*/
|
|
82
|
+
export async function transitionObjectiveNode(
|
|
83
|
+
input: ObjectiveNodeInput,
|
|
84
|
+
deps: { backend: ObjectiveNodeBackend; session: WorkflowSession },
|
|
85
|
+
): Promise<TransitionObjectiveNodeOutcome> {
|
|
86
|
+
// The completion-audit gate (model-path-only): `status:"done"` requires a non-trivial `audit`.
|
|
87
|
+
if (input.status === "done" && !isNonTrivialAudit(input.audit)) {
|
|
88
|
+
return {
|
|
89
|
+
status: "failed",
|
|
90
|
+
message:
|
|
91
|
+
`setting a node to "done" requires a completion audit (a requirement→evidence mapping of ` +
|
|
92
|
+
`at least ${MIN_AUDIT_LENGTH} characters) — confirm the work actually landed first.`,
|
|
93
|
+
errorType: "audit_required",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (input.status === undefined && input.pr === undefined && input.description === undefined) {
|
|
97
|
+
return {
|
|
98
|
+
status: "failed",
|
|
99
|
+
message: "objective_node needs a `status`, a `pr`, or a `description` to change",
|
|
100
|
+
errorType: "bad_input",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const transitioned = await deps.backend.transition({
|
|
105
|
+
objective: input.objective,
|
|
106
|
+
node: input.node,
|
|
107
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
108
|
+
...(input.pr !== undefined ? { pr: input.pr } : {}),
|
|
109
|
+
...(input.description !== undefined ? { description: input.description } : {}),
|
|
110
|
+
});
|
|
111
|
+
if (transitioned.status === "failed") return transitioned;
|
|
112
|
+
|
|
113
|
+
// Maintain the warm node-link carrier off the successful transition (best-effort: a failed
|
|
114
|
+
// append is loud via the seam's report() but never fails the tool result).
|
|
115
|
+
let claimChange: WorkflowChangeResult | null = null;
|
|
116
|
+
if (input.status !== undefined) {
|
|
117
|
+
const claim = { objective: input.objective, node: input.node };
|
|
118
|
+
claimChange =
|
|
119
|
+
input.status === "planning"
|
|
120
|
+
? deps.session.apply({ kind: "record-node-claim", claim })
|
|
121
|
+
: deps.session.apply({ kind: "clear-node-claim", claim });
|
|
122
|
+
}
|
|
123
|
+
return { status: "ok", commentUpdated: transitioned.commentUpdated, claimChange };
|
|
124
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Feature-owned prose + identity constants for the objective flows — the stage ids the
|
|
2
|
+
// providers/doors key off, the objective-authoring context (contracts.md §8.57: live state +
|
|
3
|
+
// pointers only; the flow is stated by the launch statement, the detail by the bound skill),
|
|
4
|
+
// and the seed guidance the warm doors inject (`/objective-plan`, `/objective-reconcile`,
|
|
5
|
+
// `/objective-save`). Everything renders through the shared template seam
|
|
6
|
+
// (`substrate/prompts.ts` — a mechanism import, §8.31); branching stays in code.
|
|
7
|
+
//
|
|
8
|
+
// Pi-free (guard Rule D covers `authoring/` by prefix). Tool-guideline arrays deliberately do
|
|
9
|
+
// NOT live here: they stay inline at the `pi/v1` registration sites (the prose-review
|
|
10
|
+
// workbench's source-shape constraint).
|
|
11
|
+
|
|
12
|
+
import { render } from "../../substrate/prompts.ts";
|
|
13
|
+
|
|
14
|
+
/** The registry stage id of the objective-authoring session (shared with planMode's defer check). */
|
|
15
|
+
export const OBJECTIVE_AUTHOR_STAGE = "objective-author";
|
|
16
|
+
|
|
17
|
+
/** The registry stage id of the structured-save session (`perk objective save`). */
|
|
18
|
+
export const OBJECTIVE_SAVE_STAGE = "objective-save";
|
|
19
|
+
|
|
20
|
+
/** The objective-authoring context customType (distinct from planMode's `perk:plan-context`). */
|
|
21
|
+
export const OBJECTIVE_AUTHOR_CONTEXT_TYPE = "perk:objective-author-context";
|
|
22
|
+
|
|
23
|
+
/** The dedup/strip marker carried by the injected objective-authoring context. */
|
|
24
|
+
export const OBJECTIVE_AUTHOR_MARKER = "[OBJECTIVE AUTHORING]";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The objective-authoring session context: live state + pointers only (contracts.md §8.57 — the
|
|
28
|
+
* flow is stated by the launch statement, the detail by the `perk-objective-author` skill). It
|
|
29
|
+
* names the working-draft artifact (`objective_draft`), the review tool (`plan_review`), and
|
|
30
|
+
* the bound skill; it never restates the flow. Prompting, NOT enforcement (the tool gate is the
|
|
31
|
+
* enforcement).
|
|
32
|
+
*/
|
|
33
|
+
export const OBJECTIVE_AUTHORING_CONTEXT = render("contexts/objective-authoring.md", {
|
|
34
|
+
marker: OBJECTIVE_AUTHOR_MARKER,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the full objective-authoring injection, appending the project-config addendum when
|
|
39
|
+
* present. Pure: the adapter passes `loadPerkConfig(cwd).planAuthoring` (the same `[workflow]
|
|
40
|
+
* plan_authoring` addendum the plan-authoring injection consumes — verbatim reuse).
|
|
41
|
+
*/
|
|
42
|
+
export function objectiveAuthoringContextContent(addendum: string | undefined): string {
|
|
43
|
+
return addendum
|
|
44
|
+
? `${OBJECTIVE_AUTHORING_CONTEXT}\n\n${addendum.trim()}`
|
|
45
|
+
: OBJECTIVE_AUTHORING_CONTEXT;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Backend-aware supplemental clause for the objective-read step of the factory prompts.
|
|
50
|
+
* The wording lives in `prompts/common/objective-read/linear.md`, rendered identically by both
|
|
51
|
+
* planes via the shared render seam (contracts.md §8.31); branching stays in code. github (and any
|
|
52
|
+
* non-linear) → "" (the `perk objective show` step already covers it); linear → the Project URL +
|
|
53
|
+
* the linear_get_issue/linear_list_comments tools (an `open <url>` fallback when the url is known).
|
|
54
|
+
*/
|
|
55
|
+
export function objectiveReadInstruction(
|
|
56
|
+
backend: string,
|
|
57
|
+
objectiveId: string,
|
|
58
|
+
url: string,
|
|
59
|
+
): string {
|
|
60
|
+
if (backend !== "linear") return "";
|
|
61
|
+
const where = url ? `(${url})` : `(run \`perk objective show ${objectiveId}\` for its URL)`;
|
|
62
|
+
const fallback = url ? `; if the linear tools are unavailable, open ${url}` : "";
|
|
63
|
+
return render("common/objective-read/linear.md", { where, fallback });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The seed guidance the warm `/objective-plan` injects to start the factory loop (the
|
|
67
|
+
* perk-objective-plan skill pointer rides the skill-binding suffix — not hardcoded).
|
|
68
|
+
* The loop is file-first (`plan_draft` → `plan_review` → approval-driven save); the node link
|
|
69
|
+
* rides the `objective_node_claim` carrier recorded by the unconditional `planning` mark.
|
|
70
|
+
* The OPTIONAL explore step is ONE `explore_objective_node` call — the tool owns the wave
|
|
71
|
+
* mechanics, the report schema, and reads the configured `[models.subagents] objective-explorer`
|
|
72
|
+
* model at execute time. */
|
|
73
|
+
export function factoryGuidance(
|
|
74
|
+
objective: string,
|
|
75
|
+
node: string | null,
|
|
76
|
+
backend = "github",
|
|
77
|
+
url = "",
|
|
78
|
+
): string {
|
|
79
|
+
const readClause = objectiveReadInstruction(backend, objective, url);
|
|
80
|
+
return render("stages/objective-plan/guidance.md", {
|
|
81
|
+
objective,
|
|
82
|
+
node: node ?? "",
|
|
83
|
+
read_clause: readClause,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The seed guidance the warm `/objective-reconcile` injects to start the reconcile pass (the
|
|
88
|
+
* perk-objective-reconcile skill pointer rides the skill-binding suffix — not
|
|
89
|
+
* hardcoded). */
|
|
90
|
+
export function reconcileGuidance(objective: string, backend = "github", url = ""): string {
|
|
91
|
+
const readClause = objectiveReadInstruction(backend, objective, url);
|
|
92
|
+
return render("stages/objective-reconcile.md", { objective, read_clause: readClause });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The seed guidance the warm `/objective-save` injects to drive the structured save (the
|
|
97
|
+
* perk-objective-author skill pointer rides the skill-binding suffix — not hardcoded
|
|
98
|
+
* here). Pure + exported for offline tests.
|
|
99
|
+
*/
|
|
100
|
+
export function objectiveSaveGuidance(title?: string): string {
|
|
101
|
+
const named = title?.trim() || "";
|
|
102
|
+
return render("stages/objective-save.md", { title: named });
|
|
103
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// The objective review feature: the `ObjectiveDraftReviewer` role (one production adapter per
|
|
2
|
+
// review backend — plannotator bridge or first-party editor, both built in pi/v1 — plus a
|
|
3
|
+
// scripted fake in the tests) and the one-entry `reviewObjectiveDraft` operation.
|
|
4
|
+
//
|
|
5
|
+
// Ordering is the review door's exact discipline: resume FIRST (the validated artifact only —
|
|
6
|
+
// objectives have no param/transcript tier; none ⇒ `noDraft`) → render (the reviewed bytes are
|
|
7
|
+
// ALWAYS the rendered markdown — the review-surface law: JSON is storage/transport, never the
|
|
8
|
+
// human surface) → review → the abort checkpoint → route the verdict. Provider vocabulary
|
|
9
|
+
// (plannotator's `# Direct Edits` heading) is translated INTO `ObjectiveReviewOutcome` at the
|
|
10
|
+
// adapter — the feature sees typed variants only.
|
|
11
|
+
//
|
|
12
|
+
// No implement-here arm (the no-save exit is plan-arm-only — contracts §8.23) and no
|
|
13
|
+
// edited-bytes channel: objective reviews are VIEW-ONLY (rendered-markdown edits cannot fold
|
|
14
|
+
// back into the structured draft mechanically), so `approvedDirectEdits` is the revise-round
|
|
15
|
+
// refusal arm — NOTHING saved, the gate untouched; deny+feedback is the change channel.
|
|
16
|
+
//
|
|
17
|
+
// Cancellation ownership: `signal?.aborted` is checked at entry AND re-checked immediately
|
|
18
|
+
// after `reviewer.review` resolves, BEFORE any effect (save, gate exit) — abort ⇒ the `aborted`
|
|
19
|
+
// arm with nothing saved and the gate untouched.
|
|
20
|
+
|
|
21
|
+
import type { WorkflowSession } from "../../session/workflowSession.ts";
|
|
22
|
+
import { renderObjectiveDraft, resumeObjectiveDraft } from "./draft.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The reviewer's verdict on the rendered objective bytes. `approvedDirectEdits` carries the
|
|
26
|
+
* FULL raw feedback (the adapter renders the revise-round guidance with it); the heading check
|
|
27
|
+
* that produces it stays adapter-side (provider vocabulary).
|
|
28
|
+
*/
|
|
29
|
+
export type ObjectiveReviewOutcome =
|
|
30
|
+
| { status: "approved"; feedback?: string; reviewId?: string }
|
|
31
|
+
| { status: "approvedDirectEdits"; rawFeedback: string; reviewId?: string }
|
|
32
|
+
| { status: "denied"; feedback?: string; reviewId?: string }
|
|
33
|
+
| { status: "dismissed" }
|
|
34
|
+
| { status: "aborted" }
|
|
35
|
+
| { status: "unavailable"; warning: string };
|
|
36
|
+
|
|
37
|
+
/** The reviewer role: judge the rendered objective markdown (view-only). */
|
|
38
|
+
export interface ObjectiveDraftReviewer {
|
|
39
|
+
review(rendered: string, signal?: AbortSignal): Promise<ObjectiveReviewOutcome>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The structural approval-save shape the review op routes over. The adapter binds a RICHER twin
|
|
44
|
+
* (`objectiveApprovalSaveV1` — arms carrying rendered results + `gateExited`); the arms flow
|
|
45
|
+
* through VERBATIM via the generic, so no information is erased and no cast is needed.
|
|
46
|
+
*/
|
|
47
|
+
export interface ObjectiveApprovalSaveShape {
|
|
48
|
+
status: "no-draft" | "refused-draft" | "saved" | "save-failed";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The one-entry review outcome — each arm carries exactly what its caller renders.
|
|
52
|
+
* `refusedDraft` is the pre-review fail-closed stop for an invalid artifact (the approval-time
|
|
53
|
+
* race arm rides the save shape verbatim through `approvedSave`). */
|
|
54
|
+
export type ReviewObjectiveDraftResult<A extends ObjectiveApprovalSaveShape> =
|
|
55
|
+
| { status: "noDraft" }
|
|
56
|
+
| { status: "refusedDraft"; problem: string }
|
|
57
|
+
| { status: "approvedSave"; save: A; feedback?: string; reviewId?: string }
|
|
58
|
+
| { status: "approvedDirectEdits"; rawFeedback: string; reviewId?: string }
|
|
59
|
+
| { status: "denied"; feedback?: string; reviewId?: string }
|
|
60
|
+
| { status: "dismissed" }
|
|
61
|
+
| { status: "aborted" }
|
|
62
|
+
| { status: "unavailable"; warning: string };
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Review the working objective draft end-to-end: resume (none ⇒ `noDraft` — the adapter renders
|
|
66
|
+
* the `no_objective_draft` skip) → render → review → the abort checkpoint → route. Routing:
|
|
67
|
+
* `approved` ⇒ `approvalSave()` (the injected orchestration re-resumes the draft itself — the
|
|
68
|
+
* artifact is the save source, never the rendered bytes; one extra validated read, no
|
|
69
|
+
* observable delta) · `approvedDirectEdits` ⇒ the revise-round refusal arm — NOTHING saved, the
|
|
70
|
+
* gate untouched · `denied`/`dismissed`/`aborted`/`unavailable` pass through. Never throws.
|
|
71
|
+
*/
|
|
72
|
+
export async function reviewObjectiveDraft<A extends ObjectiveApprovalSaveShape>(
|
|
73
|
+
deps: {
|
|
74
|
+
session: WorkflowSession;
|
|
75
|
+
reviewer: ObjectiveDraftReviewer;
|
|
76
|
+
approvalSave: () => Promise<A>;
|
|
77
|
+
},
|
|
78
|
+
signal?: AbortSignal,
|
|
79
|
+
): Promise<ReviewObjectiveDraftResult<A>> {
|
|
80
|
+
if (signal?.aborted) return { status: "aborted" };
|
|
81
|
+
const resumed = resumeObjectiveDraft(deps.session);
|
|
82
|
+
if (resumed.kind === "absent") return { status: "noDraft" };
|
|
83
|
+
if (resumed.kind === "refused") return { status: "refusedDraft", problem: resumed.problem };
|
|
84
|
+
|
|
85
|
+
const outcome = await deps.reviewer.review(renderObjectiveDraft(resumed.draft), signal);
|
|
86
|
+
// The abort checkpoint: a turn interrupted while the reviewer ran must produce NO effect —
|
|
87
|
+
// no save, no gate exit (the aborted arm wins over any verdict).
|
|
88
|
+
if (signal?.aborted) return { status: "aborted" };
|
|
89
|
+
|
|
90
|
+
return completeObjectiveReview(outcome, deps.approvalSave);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Subject policy only: callers authorize effects before entering this completion seam. */
|
|
94
|
+
export async function completeObjectiveReview<A extends ObjectiveApprovalSaveShape>(
|
|
95
|
+
outcome: ObjectiveReviewOutcome,
|
|
96
|
+
approvalSave: () => Promise<A>,
|
|
97
|
+
): Promise<ReviewObjectiveDraftResult<A>> {
|
|
98
|
+
switch (outcome.status) {
|
|
99
|
+
case "approved":
|
|
100
|
+
return {
|
|
101
|
+
status: "approvedSave",
|
|
102
|
+
save: await approvalSave(),
|
|
103
|
+
...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
|
|
104
|
+
...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
|
|
105
|
+
};
|
|
106
|
+
case "approvedDirectEdits":
|
|
107
|
+
// The revise-round refusal: rendered-markdown edits cannot fold back into the structured
|
|
108
|
+
// draft mechanically — nothing saved, the gate untouched; the adapter renders the
|
|
109
|
+
// revise guidance with the FULL feedback.
|
|
110
|
+
return {
|
|
111
|
+
status: "approvedDirectEdits",
|
|
112
|
+
rawFeedback: outcome.rawFeedback,
|
|
113
|
+
...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
|
|
114
|
+
};
|
|
115
|
+
case "denied":
|
|
116
|
+
return {
|
|
117
|
+
status: "denied",
|
|
118
|
+
...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
|
|
119
|
+
...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
|
|
120
|
+
};
|
|
121
|
+
case "dismissed":
|
|
122
|
+
return { status: "dismissed" };
|
|
123
|
+
case "aborted":
|
|
124
|
+
return { status: "aborted" };
|
|
125
|
+
case "unavailable":
|
|
126
|
+
return { status: "unavailable", warning: outcome.warning };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// The objective save feature: the narrow exterior `ObjectiveBackend` port (one production
|
|
2
|
+
// adapter — the `perk objective create` cold door in pi/v1 — plus one deterministic fake in the
|
|
3
|
+
// tests: the port admission rule), the `saveObjective` operation, and the shared
|
|
4
|
+
// APPROVED-review → save orchestration `objectiveApprovalSave` (the `gistApprovalSave`/
|
|
5
|
+
// `planApprovalSave` mirror — three sibling flows, deliberately unshared shapes).
|
|
6
|
+
//
|
|
7
|
+
// Identity-less saves stay legal: `session.runId === null` ⇒ the backend sees `runId: null`
|
|
8
|
+
// (the adapter omits `--run-id`) and the LINKAGE op still runs — workflow-state appends are
|
|
9
|
+
// branch-backed and identity-independent (today's behavior, preserved). Backend saves are not
|
|
10
|
+
// abortable mid-flight (stated, not changed).
|
|
11
|
+
//
|
|
12
|
+
// The §8.63 dream gate arrives INJECTED (ctx-bound by the adapter); the §8.64 transfer staging
|
|
13
|
+
// (the reviewed CANONICAL parts crossing to the Python plane) is the ADAPTER's — the port's
|
|
14
|
+
// `dreamParts` carries the gate-proven parts and the adapter stages the run-scoped
|
|
15
|
+
// `dream-report-transfer.json` handoff atomically before the cold door.
|
|
16
|
+
|
|
17
|
+
import type { WorkflowChangeResult, WorkflowSession } from "../../session/workflowSession.ts";
|
|
18
|
+
import { type ApprovalGate, saveThroughApprovalGate } from "../review/approvalGate.ts";
|
|
19
|
+
import type { DeliveryChoice } from "./draft.ts";
|
|
20
|
+
import { resumeObjectiveDraft } from "./draft.ts";
|
|
21
|
+
import type { DreamReportGateOutcome, ObjectiveDreamReportBlock } from "./dreamReportGate.ts";
|
|
22
|
+
|
|
23
|
+
/** The backend save facts (`id` is the opaque string objective id — contracts §8.21). */
|
|
24
|
+
export type ObjectiveBackendSaveResult =
|
|
25
|
+
| { status: "saved"; id: string; url: string; existed: boolean | null }
|
|
26
|
+
| { status: "failed"; message: string; errorType: string };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The narrow exterior port the save operation writes through. `runId: null` means the caller
|
|
30
|
+
* has no session identity — the adapter omits its run linkage (an identity-less save keeps
|
|
31
|
+
* working). `dreamParts` (the §8.63 gate-proven CANONICAL parts) makes the adapter stage the
|
|
32
|
+
* §8.64 transfer handoff before invoking the door.
|
|
33
|
+
*/
|
|
34
|
+
export interface ObjectiveBackend {
|
|
35
|
+
create(req: {
|
|
36
|
+
prose: string;
|
|
37
|
+
title?: string;
|
|
38
|
+
base?: string;
|
|
39
|
+
delivery?: DeliveryChoice;
|
|
40
|
+
roadmap?: unknown[];
|
|
41
|
+
runId: string | null;
|
|
42
|
+
dreamParts?: string[];
|
|
43
|
+
}): Promise<ObjectiveBackendSaveResult>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The §8.63 dream-report carrier's two proven sources, as a discriminated union so a partial
|
|
48
|
+
* "reviewed" bag cannot compile: the direct tool path supplies only the raw `input` (the save
|
|
49
|
+
* stamps `generated_at` fresh); the approval path passes the reviewed artifact block through
|
|
50
|
+
* WHOLE — stored stamp AND stored parts (byte-compared against the fresh re-render).
|
|
51
|
+
*/
|
|
52
|
+
export type DreamReportCarrier =
|
|
53
|
+
| { source: "direct"; input: unknown }
|
|
54
|
+
| { source: "reviewed"; block: ObjectiveDreamReportBlock };
|
|
55
|
+
|
|
56
|
+
/** The typed save input — decode owns the shape at the tool boundary (pi/v1). */
|
|
57
|
+
export interface SaveObjectiveInput {
|
|
58
|
+
prose: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
roadmap?: unknown[];
|
|
61
|
+
base?: string;
|
|
62
|
+
delivery?: DeliveryChoice;
|
|
63
|
+
dream_report?: DreamReportCarrier;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The save outcome. The saved arm carries the backend facts plus the session seam's own
|
|
68
|
+
* `WorkflowChangeResult` linkage VERBATIM (the `SavePlanOutcome` precedent; `null` = not
|
|
69
|
+
* attempted — a failed save never touches the session). The adapter renders the terminating
|
|
70
|
+
* "Saved/Found existing objective #id → url" twin and appends the budget activation marker iff
|
|
71
|
+
* the linkage was attempted and not `unchanged` (byte-equivalent to the historical
|
|
72
|
+
* `linked !== objectiveId` guard — applied/unverified/rejected all imply "differed", and the
|
|
73
|
+
* marker never keyed off the append's read-back result).
|
|
74
|
+
*/
|
|
75
|
+
export type SaveObjectiveOutcome =
|
|
76
|
+
| {
|
|
77
|
+
status: "saved";
|
|
78
|
+
id: string;
|
|
79
|
+
url: string;
|
|
80
|
+
existed: boolean | null;
|
|
81
|
+
linkage: WorkflowChangeResult | null;
|
|
82
|
+
}
|
|
83
|
+
| { status: "failed"; message: string; errorType: string };
|
|
84
|
+
|
|
85
|
+
/** The dependency bag the save operation runs over (the adapter composes production values). */
|
|
86
|
+
export interface ObjectiveSaveDeps {
|
|
87
|
+
session: WorkflowSession;
|
|
88
|
+
backend: ObjectiveBackend;
|
|
89
|
+
/** The §8.63 gate, adapter-bound (`resolveDreamReportGate` over the production recovery capability). */
|
|
90
|
+
resolveDreamGate: (input: unknown, generatedAt: string) => DreamReportGateOutcome;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The single save operation every objective-save surface calls. Ordering preserved exactly:
|
|
95
|
+
* validate the non-blank prose → the §8.63 fail-closed gate re-validation (presence is the
|
|
96
|
+
* `input.dream_report === undefined` boundary — an `{input: undefined}` carrier is never
|
|
97
|
+
* constructed) → the approval path's stored-parts byte-compare (the same stored `generated_at`
|
|
98
|
+
* stamp keeps the comparison deterministic; drift/tamper between draft-write and save refuses
|
|
99
|
+
* `bad_state`, nothing saved) → `backend.create` with `runId: session.runId` → on success,
|
|
100
|
+
* link the live session (`apply({kind: "link-objective"})` — the seam appends iff the rebuilt
|
|
101
|
+
* `active_objective` differs, strict read-back). Whitespace-only `title`/`base` normalize to
|
|
102
|
+
* absent (trim-or-omit, matching the draft path — a blank `--title` can no longer reach the
|
|
103
|
+
* cold door). Never throws.
|
|
104
|
+
*/
|
|
105
|
+
export async function saveObjective(
|
|
106
|
+
input: SaveObjectiveInput,
|
|
107
|
+
deps: ObjectiveSaveDeps,
|
|
108
|
+
): Promise<SaveObjectiveOutcome> {
|
|
109
|
+
const prose = input.prose.trim();
|
|
110
|
+
if (!prose) {
|
|
111
|
+
return {
|
|
112
|
+
status: "failed",
|
|
113
|
+
message: "no objective prose to save (draft the objective first)",
|
|
114
|
+
errorType: "invalid_input",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const title = input.title?.trim() || undefined;
|
|
118
|
+
const base = input.base?.trim() || undefined;
|
|
119
|
+
|
|
120
|
+
const carrier = input.dream_report;
|
|
121
|
+
const generatedAt =
|
|
122
|
+
carrier?.source === "reviewed" ? carrier.block.generated_at : new Date().toISOString();
|
|
123
|
+
const gate =
|
|
124
|
+
carrier === undefined
|
|
125
|
+
? deps.resolveDreamGate(undefined, generatedAt)
|
|
126
|
+
: deps.resolveDreamGate(
|
|
127
|
+
carrier.source === "reviewed" ? carrier.block.input : carrier.input,
|
|
128
|
+
generatedAt,
|
|
129
|
+
);
|
|
130
|
+
if (gate.kind === "refuse") {
|
|
131
|
+
return { status: "failed", message: gate.detail, errorType: gate.errorType };
|
|
132
|
+
}
|
|
133
|
+
if (gate.kind === "block" && carrier?.source === "reviewed") {
|
|
134
|
+
// The approval path: the reviewed (stored) parts must byte-match the re-render against
|
|
135
|
+
// freshly recovered context.
|
|
136
|
+
if (JSON.stringify(gate.block.parts) !== JSON.stringify(carrier.block.parts)) {
|
|
137
|
+
return {
|
|
138
|
+
status: "failed",
|
|
139
|
+
message: "the reviewed report no longer matches the wave state — re-draft and re-review",
|
|
140
|
+
errorType: "bad_state",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const saved = await deps.backend.create({
|
|
146
|
+
prose,
|
|
147
|
+
...(title !== undefined ? { title } : {}),
|
|
148
|
+
...(base !== undefined ? { base } : {}),
|
|
149
|
+
...(input.delivery !== undefined ? { delivery: input.delivery } : {}),
|
|
150
|
+
...(input.roadmap !== undefined ? { roadmap: input.roadmap } : {}),
|
|
151
|
+
runId: deps.session.runId,
|
|
152
|
+
...(gate.kind === "block" ? { dreamParts: gate.block.parts } : {}),
|
|
153
|
+
});
|
|
154
|
+
if (saved.status === "failed") return saved;
|
|
155
|
+
|
|
156
|
+
// Link the live session: the seam appends iff the rebuilt `active_objective` differs, with a
|
|
157
|
+
// strict read-back — verbatim into the outcome (the seam's report() stays the loudness
|
|
158
|
+
// channel; a linkage failure is loud-but-non-fatal, the save stands).
|
|
159
|
+
const linkage = deps.session.apply({ kind: "link-objective", objective: saved.id });
|
|
160
|
+
return { status: "saved", id: saved.id, url: saved.url, existed: saved.existed, linkage };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The approval→save dependency bag: the save deps + the gate. */
|
|
164
|
+
export interface ObjectiveApprovalSaveDeps extends ObjectiveSaveDeps {
|
|
165
|
+
gate: ApprovalGate;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The approval→save orchestration outcome (the objective `ApprovalSaveOutcome`).
|
|
169
|
+
* `refused-draft` is the fail-closed stop for an invalid artifact: nothing saved, the gate
|
|
170
|
+
* never touched — distinct from `no-draft` (the genuine draft-less fallback arm). */
|
|
171
|
+
export type ObjectiveApprovalSaveOutcome =
|
|
172
|
+
| { status: "no-draft" }
|
|
173
|
+
| { status: "refused-draft"; problem: string }
|
|
174
|
+
| {
|
|
175
|
+
status: "saved";
|
|
176
|
+
result: Extract<SaveObjectiveOutcome, { status: "saved" }>;
|
|
177
|
+
gateExited: boolean;
|
|
178
|
+
}
|
|
179
|
+
| {
|
|
180
|
+
status: "save-failed";
|
|
181
|
+
result: Extract<SaveObjectiveOutcome, { status: "failed" }>;
|
|
182
|
+
gateExited: false;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The shared APPROVED-review → save orchestration (the objective sibling of `planApprovalSave`):
|
|
187
|
+
* an APPROVED objective review (`plan_review`'s objective arm) and the manual `/objective-save`
|
|
188
|
+
* failsafe both run THIS. Flow: re-read the STRUCTURED draft artifact at save time
|
|
189
|
+
* (`resumeObjectiveDraft` — never the rendered markdown, never in-hand bytes; the artifact's
|
|
190
|
+
* `dream_report` block passes through whole: stored stamp + stored parts) → `saveObjective`
|
|
191
|
+
* through `saveThroughApprovalGate` (the D1a invariant: snapshot before the save; exit only
|
|
192
|
+
* after a successful save while read-only; a failed save leaves the gate ON). No draft → `no-draft`
|
|
193
|
+
* (nothing saved, the gate untouched); a REFUSED draft → `refused-draft` before the gate
|
|
194
|
+
* snapshot (fail-closed stop — `gateExited` semantics never arise). Title precedence: an
|
|
195
|
+
* explicit `opts.title` wins; else the draft's `title`; else the cold door derives from the
|
|
196
|
+
* prose heading.
|
|
197
|
+
*/
|
|
198
|
+
export async function objectiveApprovalSave(
|
|
199
|
+
deps: ObjectiveApprovalSaveDeps,
|
|
200
|
+
opts: { title?: string } = {},
|
|
201
|
+
): Promise<ObjectiveApprovalSaveOutcome> {
|
|
202
|
+
const resumed = resumeObjectiveDraft(deps.session);
|
|
203
|
+
if (resumed.kind === "absent") return { status: "no-draft" };
|
|
204
|
+
if (resumed.kind === "refused") return { status: "refused-draft", problem: resumed.problem };
|
|
205
|
+
const draft = resumed.draft;
|
|
206
|
+
const title = opts.title ?? draft.title;
|
|
207
|
+
const { outcome: result, gateExited } = await saveThroughApprovalGate(deps.gate, () =>
|
|
208
|
+
saveObjective(
|
|
209
|
+
{
|
|
210
|
+
prose: draft.prose,
|
|
211
|
+
...(title !== undefined ? { title } : {}),
|
|
212
|
+
roadmap: draft.roadmap,
|
|
213
|
+
...(draft.base !== undefined ? { base: draft.base } : {}),
|
|
214
|
+
...(draft.delivery !== undefined ? { delivery: draft.delivery } : {}),
|
|
215
|
+
...(draft.dream_report !== undefined
|
|
216
|
+
? { dream_report: { source: "reviewed" as const, block: draft.dream_report } }
|
|
217
|
+
: {}),
|
|
218
|
+
},
|
|
219
|
+
deps,
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
if (result.status === "failed") return { status: "save-failed", result, gateExited: false };
|
|
223
|
+
return { status: "saved", result, gateExited };
|
|
224
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The plan working-draft feature: the fixed artifact constant and the two draft operations over
|
|
2
|
+
// the WorkflowSession seam. Unlike the gist sibling there is NO encode/decode layer — the
|
|
3
|
+
// artifact is raw plan markdown (no JSON envelope), so a revision is the bytes themselves.
|
|
4
|
+
//
|
|
5
|
+
// Carve-out doctrine (mirrors `authoring/gist/`): the artifact name is the fixed constant
|
|
6
|
+
// `PLAN_DRAFT_ARTIFACT` and every byte flows through the session seam (file + verified
|
|
7
|
+
// `session_artifacts` pointer), so the only thing the draft ops can ever touch is the one
|
|
8
|
+
// working-plan artifact in the current run's data dir (gitignored scratch). A revision is a
|
|
9
|
+
// WHOLE-VALUE replacement — full rewrite per call, never a save (`plan_save`/`/plan-save` still
|
|
10
|
+
// persist to GitHub).
|
|
11
|
+
|
|
12
|
+
import type { SessionArtifactReceipt, WorkflowSession } from "../../session/workflowSession.ts";
|
|
13
|
+
|
|
14
|
+
/** The fixed working-plan artifact name (NOT `plan.md` — `cache.plan` is a different file). */
|
|
15
|
+
export const PLAN_DRAFT_ARTIFACT = "plan-draft.md";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The revise outcome. `rejected` splits by `reason` so the adapter renders the exact failure
|
|
19
|
+
* taxonomy it always had: `blank_plan` (input refused), `no_identity` (an identity-less
|
|
20
|
+
* session), and `write_refused` (the seam refused before any effect); `unverified` means an
|
|
21
|
+
* effect may have landed but the read-back proof failed. `problem` carries the caller-facing
|
|
22
|
+
* message bytes.
|
|
23
|
+
*/
|
|
24
|
+
export type RevisePlanDraftResult =
|
|
25
|
+
| { status: "revised"; receipt: SessionArtifactReceipt; bytes: number }
|
|
26
|
+
| { status: "unchanged"; receipt: SessionArtifactReceipt; bytes: number }
|
|
27
|
+
| { status: "rejected"; reason: "blank_plan" | "no_identity" | "write_refused"; problem: string }
|
|
28
|
+
| { status: "unverified"; problem: string };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Rewrite the working plan draft (a whole-value replacement) through the session seam.
|
|
32
|
+
* Diagnostic precedence preserved: a blank plan is refused FIRST, missing identity second (the
|
|
33
|
+
* identity-optional session classifies `runId: null` — an identity-less caller still opens),
|
|
34
|
+
* then the verified artifact write. A byte-identical rewrite short-circuits `unchanged` (the
|
|
35
|
+
* session engine owns the probe). Never throws.
|
|
36
|
+
*/
|
|
37
|
+
export function revisePlanDraft(
|
|
38
|
+
input: { plan: string },
|
|
39
|
+
session: WorkflowSession,
|
|
40
|
+
): RevisePlanDraftResult {
|
|
41
|
+
if (!input.plan.trim()) {
|
|
42
|
+
return {
|
|
43
|
+
status: "rejected",
|
|
44
|
+
reason: "blank_plan",
|
|
45
|
+
problem: "no plan markdown to write (pass the full working draft)",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (session.runId === null) {
|
|
49
|
+
return {
|
|
50
|
+
status: "rejected",
|
|
51
|
+
reason: "no_identity",
|
|
52
|
+
problem: "session has no run_id — cannot write the plan-draft artifact",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const bytes = Buffer.byteLength(input.plan, "utf8");
|
|
56
|
+
const written = session.writeArtifact(PLAN_DRAFT_ARTIFACT, input.plan);
|
|
57
|
+
switch (written.status) {
|
|
58
|
+
case "applied":
|
|
59
|
+
return { status: "revised", receipt: written.receipt, bytes };
|
|
60
|
+
case "unchanged":
|
|
61
|
+
return { status: "unchanged", receipt: written.receipt, bytes };
|
|
62
|
+
case "rejected":
|
|
63
|
+
return {
|
|
64
|
+
status: "rejected",
|
|
65
|
+
reason: "write_refused",
|
|
66
|
+
problem: `could not write the ${PLAN_DRAFT_ARTIFACT} artifact (see warnings)`,
|
|
67
|
+
};
|
|
68
|
+
case "unverified":
|
|
69
|
+
return {
|
|
70
|
+
status: "unverified",
|
|
71
|
+
problem: `could not write the ${PLAN_DRAFT_ARTIFACT} artifact (see warnings)`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resume the working plan draft from the session. Fail-open `null` everywhere: `absent` is the
|
|
78
|
+
* silent no-draft arm; `invalid` was already warned by the seam. Raw markdown — blankness is
|
|
79
|
+
* the RESOLVER's concern (`resolvePlanSource` treats a blank draft as no draft). Never throws.
|
|
80
|
+
*/
|
|
81
|
+
export function resumePlanDraft(session: WorkflowSession): string | null {
|
|
82
|
+
const read = session.readArtifact(PLAN_DRAFT_ARTIFACT);
|
|
83
|
+
return read.status === "found" ? read.content : null;
|
|
84
|
+
}
|