@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,672 @@
|
|
|
1
|
+
// The v1 Pi installer for the objective authoring flow (module-contracts.md's named-installer
|
|
2
|
+
// shape): `installObjectiveAuthoringBindings` owns the objective-authoring context hook pair,
|
|
3
|
+
// the `objective_draft`/`objective_save` tools, and the `/objective-save` command —
|
|
4
|
+
// registration metadata pinned by the suite's registration-parity tests. The feature logic lives in `authoring/objective/`; this
|
|
5
|
+
// module decodes at the tool boundary, builds the cold-door backend + gate + dream-gate
|
|
6
|
+
// adapters, constructs the warm-door Result envelopes, and places the feature-owned prose in Pi
|
|
7
|
+
// fields. `objectiveApprovalSaveV1` is the composed approval→save twin the review arm
|
|
8
|
+
// (`pi/v1/objectiveReview.ts`) and the browser door consume.
|
|
9
|
+
//
|
|
10
|
+
// The objective-authoring injection rides the shared `installInjectedContext` helper
|
|
11
|
+
// (pi/v1/contextInjection.ts — contracts §8.31 semantics): a live copy in the
|
|
12
|
+
// compaction-active window suppresses re-injection, and compaction dropping it from model
|
|
13
|
+
// context re-injects on the next turn.
|
|
14
|
+
//
|
|
15
|
+
// Format doctrine (rides the tools): JSON is the storage/transport format, NEVER the human
|
|
16
|
+
// review surface — the review arm renders markdown via the feature's resume+render helpers; the
|
|
17
|
+
// approval→save orchestration re-reads the STRUCTURED artifact. Carve-out doctrine: the draft
|
|
18
|
+
// tool takes NO path/name parameter (the artifact name is fixed and the bytes flow through the
|
|
19
|
+
// session seam), so allowlisting `objective_draft` in `READ_ONLY_TOOLS` keeps the read-only
|
|
20
|
+
// invariant intact.
|
|
21
|
+
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import {
|
|
25
|
+
DELIVERY_CHOICES,
|
|
26
|
+
isDeliveryChoice,
|
|
27
|
+
OBJECTIVE_DRAFT_ARTIFACT,
|
|
28
|
+
type ObjectiveDraftInput,
|
|
29
|
+
reviseObjectiveDraft,
|
|
30
|
+
} from "../../authoring/objective/draft.ts";
|
|
31
|
+
import {
|
|
32
|
+
type DreamReportGateOutcome,
|
|
33
|
+
resolveDreamReportGate,
|
|
34
|
+
} from "../../authoring/objective/dreamReportGate.ts";
|
|
35
|
+
import {
|
|
36
|
+
OBJECTIVE_AUTHOR_CONTEXT_TYPE,
|
|
37
|
+
OBJECTIVE_AUTHOR_MARKER,
|
|
38
|
+
OBJECTIVE_AUTHOR_STAGE,
|
|
39
|
+
objectiveAuthoringContextContent,
|
|
40
|
+
objectiveSaveGuidance,
|
|
41
|
+
} from "../../authoring/objective/prose.ts";
|
|
42
|
+
import {
|
|
43
|
+
type ObjectiveApprovalSaveDeps,
|
|
44
|
+
type ObjectiveBackend,
|
|
45
|
+
objectiveApprovalSave,
|
|
46
|
+
type SaveObjectiveOutcome,
|
|
47
|
+
saveObjective,
|
|
48
|
+
} from "../../authoring/objective/save.ts";
|
|
49
|
+
import type { ApprovalGate } from "../../authoring/review/approvalGate.ts";
|
|
50
|
+
import { DREAM_REPORT_INPUT_SCHEMA } from "../../learning/dreamReport.ts";
|
|
51
|
+
import { openBranchWorkflowSession } from "../../session/branchWorkflowSession.ts";
|
|
52
|
+
import type { WorkflowSession } from "../../session/workflowSession.ts";
|
|
53
|
+
import { bindingSuffix } from "../../substrate/bindingDelivery.ts";
|
|
54
|
+
import { atomicWriteFileSync, ensureRunScratch } from "../../substrate/cache.ts";
|
|
55
|
+
import {
|
|
56
|
+
booleanField,
|
|
57
|
+
type ColdJson,
|
|
58
|
+
objectField,
|
|
59
|
+
runColdDoor,
|
|
60
|
+
stringField,
|
|
61
|
+
} from "../../substrate/coldDoor.ts";
|
|
62
|
+
import { registerPerkCommand } from "../../substrate/command.ts";
|
|
63
|
+
import { loadPerkConfig } from "../../substrate/config.ts";
|
|
64
|
+
import { failFor, ok, type Result } from "../../substrate/result.ts";
|
|
65
|
+
import type { ToolGating } from "../../substrate/toolGating.ts";
|
|
66
|
+
import { arrayParam, objectParam, paramsOf, stringParam } from "../../substrate/toolParams.ts";
|
|
67
|
+
import { type BranchEntry, branchOf, rebuildWorkflowState } from "../../substrate/workflowState.ts";
|
|
68
|
+
import { report, type Severity } from "../../surfaces/report.ts";
|
|
69
|
+
import { installInjectedContext } from "./contextInjection.ts";
|
|
70
|
+
import { type DraftReviewSlot, recordSaveOutcome } from "./draftReview.ts";
|
|
71
|
+
import { OBJECTIVE_BUDGET_TYPE } from "./objective.ts";
|
|
72
|
+
import { productionDreamGateRecovery } from "./objectiveDreamGate.ts";
|
|
73
|
+
import { isRefinementSession, refinementStageRefusal } from "./objectiveRefinement.ts";
|
|
74
|
+
|
|
75
|
+
// ------------------------------------------------------------------- the tool-boundary decode
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The decoded `objective_save` tool params (shared with `objective_draft`) — an ALIAS of the
|
|
79
|
+
* feature's draft input, not a second handwritten contract: the tool boundary decodes exactly
|
|
80
|
+
* the shape the feature operations consume (`dream_report` stays opaque here — deep validation
|
|
81
|
+
* is the gate resolver's; the save path wraps it as its `{input}` carrier arm).
|
|
82
|
+
*/
|
|
83
|
+
export type ObjectiveSaveParams = ObjectiveDraftInput;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The `delivery` enum property, shared between `objective_save` and `objective_draft` so the
|
|
87
|
+
* two tools' delivery contracts cannot drift. The description bakes in the explicit-human-choice
|
|
88
|
+
* discipline: the agent must ASK, with incremental recommended.
|
|
89
|
+
*/
|
|
90
|
+
export const DELIVERY_PARAM_SCHEMA = {
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: DELIVERY_CHOICES,
|
|
93
|
+
description:
|
|
94
|
+
"The reviewed delivery choice — ask the human explicitly (incremental is the recommended " +
|
|
95
|
+
"default: each plan lands independently; stacked lands ALL non-skipped roadmap nodes as " +
|
|
96
|
+
"one atomic PR train — capability-checked at save).",
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The `dream_report` property, shared between `objective_save` and `objective_draft` so the
|
|
101
|
+
* two tools' dream contracts cannot drift: the §8.62 `DREAM_REPORT_INPUT_SCHEMA` embedded by
|
|
102
|
+
* identifier (the `DELIVERY_PARAM_SCHEMA`/`ROADMAP_PARAM_SCHEMA` shared-schema pattern) plus
|
|
103
|
+
* the gate description. Structurally reachable only inside a `perk learn dream` session (the
|
|
104
|
+
* resolver refuses it outside one).
|
|
105
|
+
*/
|
|
106
|
+
export const DREAM_REPORT_PARAM_SCHEMA = {
|
|
107
|
+
...DREAM_REPORT_INPUT_SCHEMA,
|
|
108
|
+
description:
|
|
109
|
+
"The perk learn dream session's final report input (the parent's decisions only) — " +
|
|
110
|
+
"required inside a dream session, refused outside one.",
|
|
111
|
+
} as const;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The roadmap-node items JSON schema, shared between `objective_save` and `objective_draft`
|
|
115
|
+
* so the two tools' roadmap contracts cannot drift.
|
|
116
|
+
*/
|
|
117
|
+
export const ROADMAP_PARAM_SCHEMA = {
|
|
118
|
+
type: "object",
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
required: ["id", "description"],
|
|
121
|
+
properties: {
|
|
122
|
+
id: { type: "string", description: 'A stable node id, e.g. "1.1".' },
|
|
123
|
+
description: { type: "string", description: "What this node delivers." },
|
|
124
|
+
status: {
|
|
125
|
+
type: "string",
|
|
126
|
+
enum: ["pending", "planning", "in_progress", "done", "blocked", "skipped"],
|
|
127
|
+
description: "Optional initial status (defaults to pending).",
|
|
128
|
+
},
|
|
129
|
+
slug: { type: "string", description: "Optional short slug." },
|
|
130
|
+
pr: { type: "string", description: 'Optional plan/PR backlink, e.g. "#42".' },
|
|
131
|
+
depends_on: {
|
|
132
|
+
type: "array",
|
|
133
|
+
items: { type: "string" },
|
|
134
|
+
description: "Optional explicit dependency node ids.",
|
|
135
|
+
},
|
|
136
|
+
comment: { type: "string", description: "Optional note." },
|
|
137
|
+
adopt_issue: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description:
|
|
140
|
+
"Optional: the id/identifier of a pre-existing source issue this node adopts in place " +
|
|
141
|
+
"(objective author --from, Linear only).",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
} as const;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Decode unknown `objective_save` tool-call params (the tool-boundary seam). `prose`
|
|
148
|
+
* absent decodes to `""` (so `saveObjective`'s "no objective prose to save" `invalid_input` arm
|
|
149
|
+
* keeps owning that message) but present-but-mistyped → null (strict-fail). `roadmap` stays
|
|
150
|
+
* `unknown[]` — the Python cold door owns node-shape validation.
|
|
151
|
+
*/
|
|
152
|
+
export function decodeObjectiveSaveParams(params: unknown): ObjectiveSaveParams | null {
|
|
153
|
+
const p = paramsOf(params);
|
|
154
|
+
if (p === null) return null;
|
|
155
|
+
const prose = stringParam(p, "prose");
|
|
156
|
+
const title = stringParam(p, "title");
|
|
157
|
+
const roadmap = arrayParam(p, "roadmap");
|
|
158
|
+
const base = stringParam(p, "base");
|
|
159
|
+
const delivery = stringParam(p, "delivery");
|
|
160
|
+
// `dream_report` must be a plain object when present (absent → undefined); deep validation
|
|
161
|
+
// stays with the gate resolver (resolveDreamReportGate).
|
|
162
|
+
const dreamReport = objectParam(p, "dream_report");
|
|
163
|
+
if (
|
|
164
|
+
prose === null ||
|
|
165
|
+
title === null ||
|
|
166
|
+
roadmap === null ||
|
|
167
|
+
base === null ||
|
|
168
|
+
delivery === null ||
|
|
169
|
+
dreamReport === null
|
|
170
|
+
) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
// The delivery enum is strict beyond `string`: an off-enum value is present-but-mistyped.
|
|
174
|
+
if (delivery !== undefined && !isDeliveryChoice(delivery)) return null;
|
|
175
|
+
return { prose: prose ?? "", title, roadmap, base, delivery, dream_report: dreamReport };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// -------------------------------------------------------------- the cold-door backend adapter
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The run-scoped dream-report transfer filename (contracts §8.64) — the extension→door handoff
|
|
182
|
+
* carrying the reviewed CANONICAL parts. The Python mirror is
|
|
183
|
+
* `perk.learn.dream_companion.DREAM_REPORT_TRANSFER_FILENAME` (parity-pinned by test), beside
|
|
184
|
+
* the existing `DREAM_MANIFEST_FILENAME` mirror pair.
|
|
185
|
+
*/
|
|
186
|
+
export const DREAM_REPORT_TRANSFER_FILENAME = "dream-report-transfer.json";
|
|
187
|
+
|
|
188
|
+
/** The ok-arm fields — the structured `details` surface doubles as branch-safe persisted state. */
|
|
189
|
+
export interface ObjectiveSaveOk {
|
|
190
|
+
/** `id` is the opaque string objective id (GitHub "7", Linear "ENG-7") — §8.21. */
|
|
191
|
+
objective: { id: string; url: string };
|
|
192
|
+
existed: boolean | null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export type ObjectiveSaveResult = Result<ObjectiveSaveOk>;
|
|
196
|
+
|
|
197
|
+
/** The decoded `perk objective create --json` payload slice the warm door consumes. */
|
|
198
|
+
interface ObjectiveCreatePayload {
|
|
199
|
+
objective: { id: string; url: string; existed: boolean | undefined };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Narrow the `perk objective create --json` success payload; strict on `objective`. */
|
|
203
|
+
function decodeObjectiveCreate(payload: ColdJson): ObjectiveCreatePayload | null {
|
|
204
|
+
const objective = objectField(payload, "objective");
|
|
205
|
+
if (objective === undefined) return null;
|
|
206
|
+
const id = stringField(objective, "id");
|
|
207
|
+
const url = stringField(objective, "url");
|
|
208
|
+
if (id === undefined || url === undefined) return null;
|
|
209
|
+
return { objective: { id, url, existed: booleanField(objective, "existed") } };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The production `ObjectiveBackend` over the Python cold door (`perk objective create --json`
|
|
214
|
+
* via the shared cold-door client; the prose rides the run-scratch stdin channel; `runId: null`
|
|
215
|
+
* or blank omits `--run-id` — an identity-less save keeps working). On the dream arm
|
|
216
|
+
* (`dreamParts` present) the reviewed CANONICAL parts cross to the Python plane through the
|
|
217
|
+
* run-scoped `dream-report-transfer.json` handoff (§8.64) — staged atomically BEFORE the cold
|
|
218
|
+
* door (a write failure is the soft `scratch_failed` refusal, the runColdDoor stdin-staging
|
|
219
|
+
* precedent: the door is NOT invoked, nothing activates, the read-only gate stays on) — and
|
|
220
|
+
* `perk objective create` re-validates the transfer and converges the companion idempotently.
|
|
221
|
+
* Non-dream saves write nothing (byte-identical).
|
|
222
|
+
*/
|
|
223
|
+
function coldDoorObjectiveBackend(pi: ExtensionAPI, ctx: ExtensionContext): ObjectiveBackend {
|
|
224
|
+
return {
|
|
225
|
+
async create(req) {
|
|
226
|
+
const runId = req.runId ?? "";
|
|
227
|
+
if (req.dreamParts !== undefined) {
|
|
228
|
+
try {
|
|
229
|
+
const dir = ensureRunScratch(ctx.cwd, runId);
|
|
230
|
+
const content = `${JSON.stringify(
|
|
231
|
+
{ schema_version: "1", run_id: runId, parts: req.dreamParts },
|
|
232
|
+
null,
|
|
233
|
+
2,
|
|
234
|
+
)}\n`;
|
|
235
|
+
atomicWriteFileSync(join(dir, DREAM_REPORT_TRANSFER_FILENAME), content);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
return {
|
|
238
|
+
status: "failed",
|
|
239
|
+
message: `could not stage the dream-report transfer: ${String(err)}`,
|
|
240
|
+
errorType: "scratch_failed",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const args = ["objective", "create", "--json"];
|
|
245
|
+
if (req.title) args.push("--title", req.title);
|
|
246
|
+
if (req.base) args.push("--base", req.base);
|
|
247
|
+
// The reviewed delivery choice rides verbatim; the cold door owns validation + preflight.
|
|
248
|
+
if (req.delivery) args.push("--delivery", req.delivery);
|
|
249
|
+
if (runId) args.push("--run-id", runId);
|
|
250
|
+
if (req.roadmap && req.roadmap.length > 0) {
|
|
251
|
+
args.push("--roadmap", JSON.stringify(req.roadmap));
|
|
252
|
+
}
|
|
253
|
+
const r = await runColdDoor<ObjectiveCreatePayload>(pi, ctx, args, {
|
|
254
|
+
label: "perk objective create",
|
|
255
|
+
decode: decodeObjectiveCreate,
|
|
256
|
+
stdin: { flag: "--body", content: req.prose, filename: "objective.md" },
|
|
257
|
+
});
|
|
258
|
+
if (!r.ok) return { status: "failed", message: r.message, errorType: r.errorType };
|
|
259
|
+
return {
|
|
260
|
+
status: "saved",
|
|
261
|
+
id: r.data.objective.id,
|
|
262
|
+
url: r.data.objective.url,
|
|
263
|
+
existed: r.data.objective.existed ?? null,
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ------------------------------------------------------------------------- adapter plumbing
|
|
270
|
+
|
|
271
|
+
/** Open the branch-backed session (always opens; `runId: null` is the identity-less arm). */
|
|
272
|
+
function openSession(pi: ExtensionAPI, ctx: ExtensionContext): WorkflowSession {
|
|
273
|
+
return openBranchWorkflowSession(pi, ctx);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The narrow gate slice the feature releases (D1a: exit only after a verified save). */
|
|
277
|
+
function gateFor(gating: ToolGating, ctx: ExtensionContext): ApprovalGate {
|
|
278
|
+
return { isActive: () => gating.isActive(), exit: () => gating.exit(ctx) };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The ctx-bound §8.63 gate resolver the feature ops consume — the resolver over the
|
|
282
|
+
* runtime-minted production recovery capability (`pi/v1/objectiveDreamGate.ts`). */
|
|
283
|
+
function dreamGateFor(
|
|
284
|
+
ctx: ExtensionContext,
|
|
285
|
+
): (input: unknown, generatedAt: string) => DreamReportGateOutcome {
|
|
286
|
+
return (input, generatedAt) =>
|
|
287
|
+
resolveDreamReportGate(productionDreamGateRecovery(ctx), input, generatedAt);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The production approval→save dependency bag (the `planSaveDepsFor` mirror): session via the
|
|
292
|
+
* branch backing, the cold-door backend, the gate slice, and the ctx-bound dream-gate resolver.
|
|
293
|
+
* The review arm + the browser door consume the composed `objectiveApprovalSaveV1` instead.
|
|
294
|
+
*/
|
|
295
|
+
export function objectiveSaveDepsFor(
|
|
296
|
+
pi: ExtensionAPI,
|
|
297
|
+
ctx: ExtensionContext,
|
|
298
|
+
gating: ToolGating,
|
|
299
|
+
): ObjectiveApprovalSaveDeps {
|
|
300
|
+
return {
|
|
301
|
+
session: openSession(pi, ctx),
|
|
302
|
+
backend: coldDoorObjectiveBackend(pi, ctx),
|
|
303
|
+
gate: gateFor(gating, ctx),
|
|
304
|
+
resolveDreamGate: dreamGateFor(ctx),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Seed a fresh `perk:objective-budget` activation marker off a successful save whose linkage
|
|
310
|
+
* differed (mirrors `/objective <id>`'s activation, so budget tracking starts immediately).
|
|
311
|
+
* Byte-equivalent to the historical `linked !== objectiveId` guard: the seam's `unchanged` is
|
|
312
|
+
* the equal case, and applied/unverified/rejected all imply "differed" (the marker never keyed
|
|
313
|
+
* off the append's read-back result).
|
|
314
|
+
*/
|
|
315
|
+
function activateBudgetIfLinked(pi: ExtensionAPI, save: SaveObjectiveOutcome): void {
|
|
316
|
+
if (save.status !== "saved") return;
|
|
317
|
+
if (save.linkage === null || save.linkage.status === "unchanged") return;
|
|
318
|
+
pi.appendEntry(OBJECTIVE_BUDGET_TYPE, {
|
|
319
|
+
objective_id: save.id,
|
|
320
|
+
activated_at: new Date().toISOString(),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Render a save outcome as the warm-door Result envelope (the ONE result-construction site for
|
|
326
|
+
* every objective save surface — tool, command relay, review arm, browser door). A `saved`
|
|
327
|
+
* outcome renders the terminating "Saved/Found existing objective #id → url" twin; a `failed`
|
|
328
|
+
* outcome reports through the `failFor` seam (byte-identical to the failure the save always
|
|
329
|
+
* rendered).
|
|
330
|
+
*/
|
|
331
|
+
function objectiveSaveResultOf(
|
|
332
|
+
ctx: ExtensionContext,
|
|
333
|
+
save: SaveObjectiveOutcome,
|
|
334
|
+
): ObjectiveSaveResult {
|
|
335
|
+
if (save.status === "failed") {
|
|
336
|
+
return failFor(ctx, "objective-save")(save.message, save.errorType);
|
|
337
|
+
}
|
|
338
|
+
const verb = save.existed ? "Found existing" : "Saved";
|
|
339
|
+
return ok(
|
|
340
|
+
`${verb} objective #${save.id} → ${save.url}`,
|
|
341
|
+
{
|
|
342
|
+
objective: { id: save.id, url: save.url },
|
|
343
|
+
existed: save.existed,
|
|
344
|
+
},
|
|
345
|
+
{ terminate: true },
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** The approval→save orchestration outcome, rendered (the door/arm-facing twin).
|
|
350
|
+
* `refused-draft` passes through unrendered — no `result`, no `gateExited`: nothing was saved,
|
|
351
|
+
* the gate was never touched, and no budget activation runs. */
|
|
352
|
+
export type ObjectiveApprovalSaveV1Outcome =
|
|
353
|
+
| { status: "no-draft" }
|
|
354
|
+
| { status: "refused-draft"; problem: string }
|
|
355
|
+
| { status: "saved" | "save-failed"; result: ObjectiveSaveResult; gateExited: boolean };
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The composed approval→save twin (the shape `plan_review`'s objective arm, the browser door,
|
|
359
|
+
* and the `/objective-save` failsafe consume): run the feature's `objectiveApprovalSave` over
|
|
360
|
+
* the production deps, seed the budget activation marker off a linked save, and render the
|
|
361
|
+
* Result envelope. Flow semantics live in the feature op (artifact re-read, D1a gate exit,
|
|
362
|
+
* §8.63 re-validation); this twin owns only composition + rendering.
|
|
363
|
+
*/
|
|
364
|
+
export async function objectiveApprovalSaveV1(
|
|
365
|
+
pi: ExtensionAPI,
|
|
366
|
+
ctx: ExtensionContext,
|
|
367
|
+
gating: ToolGating,
|
|
368
|
+
opts: { title?: string } = {},
|
|
369
|
+
): Promise<ObjectiveApprovalSaveV1Outcome> {
|
|
370
|
+
return renderObjectiveApprovalSave(
|
|
371
|
+
pi,
|
|
372
|
+
ctx,
|
|
373
|
+
await objectiveApprovalSave(objectiveSaveDepsFor(pi, ctx, gating), opts),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Preserve the subject's linkage-budget and rendered save policy for explicit owned sessions. */
|
|
378
|
+
export function renderObjectiveApprovalSave(
|
|
379
|
+
pi: ExtensionAPI,
|
|
380
|
+
ctx: ExtensionContext,
|
|
381
|
+
outcome: Awaited<ReturnType<typeof objectiveApprovalSave>>,
|
|
382
|
+
): ObjectiveApprovalSaveV1Outcome {
|
|
383
|
+
if (outcome.status === "no-draft") return { status: "no-draft" };
|
|
384
|
+
if (outcome.status === "refused-draft") {
|
|
385
|
+
return { status: "refused-draft", problem: outcome.problem };
|
|
386
|
+
}
|
|
387
|
+
activateBudgetIfLinked(pi, outcome.result);
|
|
388
|
+
return {
|
|
389
|
+
status: outcome.status,
|
|
390
|
+
result: objectiveSaveResultOf(ctx, outcome.result),
|
|
391
|
+
gateExited: outcome.gateExited,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Whether the current branch is an objective-author session (read-only gate AND stage match).
|
|
397
|
+
* Fail-closed: a throwing state rebuild reports false, so no objective-authoring guidance is
|
|
398
|
+
* selected. (The runner fence lives in the shared injection helper.)
|
|
399
|
+
*/
|
|
400
|
+
function isObjectiveAuthoring(gating: ToolGating, branch: readonly BranchEntry[]): boolean {
|
|
401
|
+
if (!gating.isActive()) return false;
|
|
402
|
+
try {
|
|
403
|
+
return rebuildWorkflowState(branch).stage === OBJECTIVE_AUTHOR_STAGE;
|
|
404
|
+
} catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ------------------------------------------------------------------------------ the installer
|
|
410
|
+
|
|
411
|
+
const DRAFT_TOOL_GUIDELINES = [
|
|
412
|
+
"Call objective_draft to persist the current working objective as you author or revise it; pass the FULL prose and the FULL structured roadmap each time (it rewrites the whole draft).",
|
|
413
|
+
"objective_draft never saves to GitHub and never ends the turn — objective_save//objective-save remain the canonical save surface. Never hand-write roadmap YAML — hand the structured roadmap to the tool.",
|
|
414
|
+
"Pass objective_draft's `base` only to target a non-default branch; omit it to use the repo default.",
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
const SAVE_TOOL_GUIDELINES = [
|
|
418
|
+
"Use objective_save only after the objective + roadmap are decision-complete; it creates the canonical perk:objective issue, activates it, and ends the turn.",
|
|
419
|
+
"Pass objective_save the objective PROSE in `prose` and the STRUCTURED roadmap in `roadmap` (a JSON array of nodes) — never hand-write roadmap YAML.",
|
|
420
|
+
'Each objective_save roadmap node needs a stable `id` (e.g. "1.1") and a `description`; `status` defaults to pending. Use `depends_on` for explicit ordering.',
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Install every objective-authoring Pi binding: the objective-authoring context hook pair (the
|
|
425
|
+
* frozen hooks-ordering slot index.ts calls this at — planMode.ts defers when the stage is
|
|
426
|
+
* objective-author, so exactly one authoring context is injected), the `objective_draft` and
|
|
427
|
+
* `objective_save` tools, and the `/objective-save` command — registration metadata pinned by
|
|
428
|
+
* the registration-parity tests. Inert outside objective sessions; never throws.
|
|
429
|
+
*/
|
|
430
|
+
export function installObjectiveAuthoringBindings(
|
|
431
|
+
pi: ExtensionAPI,
|
|
432
|
+
gating: ToolGating,
|
|
433
|
+
reviews: DraftReviewSlot,
|
|
434
|
+
runnerChild: () => boolean,
|
|
435
|
+
): void {
|
|
436
|
+
// The objective-authoring context injection (display:false), keyed off (read-only gate AND
|
|
437
|
+
// stage === objective-author); the inject/strip mechanics (active-window dedup,
|
|
438
|
+
// selection-driven retention) and the runner fence live in the shared helper.
|
|
439
|
+
installInjectedContext(
|
|
440
|
+
pi,
|
|
441
|
+
{
|
|
442
|
+
customType: OBJECTIVE_AUTHOR_CONTEXT_TYPE,
|
|
443
|
+
flavors: {
|
|
444
|
+
[OBJECTIVE_AUTHOR_MARKER]: (ctx) =>
|
|
445
|
+
objectiveAuthoringContextContent(loadPerkConfig(ctx.cwd).planAuthoring),
|
|
446
|
+
},
|
|
447
|
+
select: (_ctx, branch) =>
|
|
448
|
+
isObjectiveAuthoring(gating, branch) ? OBJECTIVE_AUTHOR_MARKER : null,
|
|
449
|
+
},
|
|
450
|
+
runnerChild,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
pi.registerTool({
|
|
454
|
+
name: "objective_draft",
|
|
455
|
+
label: "Objective draft",
|
|
456
|
+
description:
|
|
457
|
+
"Write (or overwrite) the working objective draft — prose + the structured roadmap — to " +
|
|
458
|
+
"the session data dir and record its provenance pointer. The only sanctioned write surface " +
|
|
459
|
+
"while read-only. NOT a save — objective_save//objective-save still persist the objective " +
|
|
460
|
+
"to GitHub.",
|
|
461
|
+
promptSnippet:
|
|
462
|
+
"Persist the working objective draft (prose + structured roadmap) to the session data dir (full rewrite)",
|
|
463
|
+
promptGuidelines: DRAFT_TOOL_GUIDELINES,
|
|
464
|
+
executionMode: "sequential",
|
|
465
|
+
parameters: {
|
|
466
|
+
type: "object",
|
|
467
|
+
additionalProperties: false,
|
|
468
|
+
required: ["prose"],
|
|
469
|
+
properties: {
|
|
470
|
+
prose: {
|
|
471
|
+
type: "string",
|
|
472
|
+
description: "The objective prose (the why, the design, the boundaries/non-goals).",
|
|
473
|
+
},
|
|
474
|
+
title: {
|
|
475
|
+
type: "string",
|
|
476
|
+
description: "Optional objective title (defaults to the prose's first heading).",
|
|
477
|
+
},
|
|
478
|
+
base: {
|
|
479
|
+
type: "string",
|
|
480
|
+
description:
|
|
481
|
+
"Optional target branch for this objective's plans (omit to use the repo default).",
|
|
482
|
+
},
|
|
483
|
+
delivery: DELIVERY_PARAM_SCHEMA,
|
|
484
|
+
dream_report: DREAM_REPORT_PARAM_SCHEMA,
|
|
485
|
+
roadmap: {
|
|
486
|
+
type: "array",
|
|
487
|
+
description:
|
|
488
|
+
"The structured roadmap: a JSON array of nodes. Never hand-write roadmap YAML.",
|
|
489
|
+
items: ROADMAP_PARAM_SCHEMA,
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
494
|
+
// The shared param contract: the same decode as `objective_save`, so the two cannot
|
|
495
|
+
// drift. (The parameter literals are duplicated at both registration sites on purpose —
|
|
496
|
+
// the prose-review workbench needs in-place literals — and pinned identical by the
|
|
497
|
+
// registration baselines.)
|
|
498
|
+
const decoded = decodeObjectiveSaveParams(params);
|
|
499
|
+
if (decoded === null) {
|
|
500
|
+
return failFor(
|
|
501
|
+
ctx,
|
|
502
|
+
"objective-draft",
|
|
503
|
+
"objective_draft",
|
|
504
|
+
)(
|
|
505
|
+
"objective_draft needs { prose: string, roadmap?: array } per the tool schema",
|
|
506
|
+
"bad_input",
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const fail = failFor(ctx, "objective-draft");
|
|
510
|
+
const revised = reviseObjectiveDraft(decoded, {
|
|
511
|
+
session: openSession(pi, ctx),
|
|
512
|
+
resolveDreamGate: dreamGateFor(ctx),
|
|
513
|
+
});
|
|
514
|
+
switch (revised.status) {
|
|
515
|
+
case "revised":
|
|
516
|
+
case "unchanged":
|
|
517
|
+
// A byte-identical rewrite short-circuits interior-side; the rendered result is
|
|
518
|
+
// computed from identical content either way, so the surface stays byte-stable.
|
|
519
|
+
return ok(
|
|
520
|
+
`Objective draft written → ${revised.receipt.path} (${revised.receipt.digest}; ` +
|
|
521
|
+
`${revised.roadmapNodes} roadmap nodes)`,
|
|
522
|
+
{
|
|
523
|
+
name: OBJECTIVE_DRAFT_ARTIFACT,
|
|
524
|
+
path: revised.receipt.path,
|
|
525
|
+
digest: revised.receipt.digest,
|
|
526
|
+
bytes: revised.bytes,
|
|
527
|
+
run_id: revised.receipt.runId,
|
|
528
|
+
roadmap_nodes: revised.roadmapNodes,
|
|
529
|
+
},
|
|
530
|
+
);
|
|
531
|
+
case "rejected":
|
|
532
|
+
return fail(revised.problem, revised.errorType);
|
|
533
|
+
case "unverified":
|
|
534
|
+
return fail(revised.problem, "write_failed");
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
pi.registerTool({
|
|
540
|
+
name: "objective_save",
|
|
541
|
+
label: "Save objective",
|
|
542
|
+
description:
|
|
543
|
+
"Persist a drafted objective + structured roadmap to GitHub as a perk:objective issue, " +
|
|
544
|
+
"activate it, and start budget tracking. Terminating: ends the turn on save. Call only when " +
|
|
545
|
+
"the objective and roadmap are decision-complete.",
|
|
546
|
+
promptSnippet: "Save the decision-complete objective + roadmap to GitHub (terminates the turn)",
|
|
547
|
+
promptGuidelines: SAVE_TOOL_GUIDELINES,
|
|
548
|
+
executionMode: "sequential",
|
|
549
|
+
parameters: {
|
|
550
|
+
type: "object",
|
|
551
|
+
additionalProperties: false,
|
|
552
|
+
required: ["prose"],
|
|
553
|
+
properties: {
|
|
554
|
+
prose: {
|
|
555
|
+
type: "string",
|
|
556
|
+
description: "The objective prose (the why, the design, the boundaries/non-goals).",
|
|
557
|
+
},
|
|
558
|
+
title: {
|
|
559
|
+
type: "string",
|
|
560
|
+
description: "Optional objective title (defaults to the prose's first heading).",
|
|
561
|
+
},
|
|
562
|
+
base: {
|
|
563
|
+
type: "string",
|
|
564
|
+
description:
|
|
565
|
+
"Optional target branch for this objective's plans (omit to use the repo default).",
|
|
566
|
+
},
|
|
567
|
+
delivery: DELIVERY_PARAM_SCHEMA,
|
|
568
|
+
dream_report: DREAM_REPORT_PARAM_SCHEMA,
|
|
569
|
+
roadmap: {
|
|
570
|
+
type: "array",
|
|
571
|
+
description:
|
|
572
|
+
"The structured roadmap: a JSON array of nodes. Never hand-write roadmap YAML.",
|
|
573
|
+
items: ROADMAP_PARAM_SCHEMA,
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
578
|
+
const decoded = decodeObjectiveSaveParams(params);
|
|
579
|
+
if (decoded === null) {
|
|
580
|
+
return failFor(
|
|
581
|
+
ctx,
|
|
582
|
+
"objective-save",
|
|
583
|
+
"objective_save",
|
|
584
|
+
)(
|
|
585
|
+
"objective_save needs { prose: string, roadmap?: array } per the tool schema",
|
|
586
|
+
"bad_input",
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
// A refinement session never creates an objective — independent of tool visibility.
|
|
590
|
+
if (isRefinementSession(branchOf(ctx)))
|
|
591
|
+
return failFor(ctx, "objective_save")(
|
|
592
|
+
refinementStageRefusal("objective_save"),
|
|
593
|
+
"wrong_stage",
|
|
594
|
+
);
|
|
595
|
+
// The direct tool path wraps ONLY a present decoded value as the union's `direct` arm
|
|
596
|
+
// (the save stamps generated_at); no stored parts, so no byte-compare on this path.
|
|
597
|
+
const { dream_report, ...rest } = decoded;
|
|
598
|
+
const save = await saveObjective(
|
|
599
|
+
{
|
|
600
|
+
...rest,
|
|
601
|
+
...(dream_report !== undefined
|
|
602
|
+
? { dream_report: { source: "direct" as const, input: dream_report } }
|
|
603
|
+
: {}),
|
|
604
|
+
},
|
|
605
|
+
objectiveSaveDepsFor(pi, ctx, gating),
|
|
606
|
+
);
|
|
607
|
+
// The manual save never consults the latch (it IS the deliberate retry) but reports into it.
|
|
608
|
+
recordSaveOutcome(reviews, "objective", {
|
|
609
|
+
confirmed: save.status === "saved",
|
|
610
|
+
...(save.status === "failed" ? { detail: save.message } : {}),
|
|
611
|
+
});
|
|
612
|
+
activateBudgetIfLinked(pi, save);
|
|
613
|
+
return objectiveSaveResultOf(ctx, save);
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
registerPerkCommand(pi, "objective-save", {
|
|
618
|
+
description:
|
|
619
|
+
"Save the working objective draft to GitHub — the manual failsafe for the approval→save " +
|
|
620
|
+
"flow (artifact-first; drives the structured save only when no draft exists).",
|
|
621
|
+
handler: async (args, ctx) => {
|
|
622
|
+
if (isRefinementSession(branchOf(ctx))) {
|
|
623
|
+
report(ctx, "objective-save", "warning", refinementStageRefusal("/objective-save"));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const title = args.trim() || undefined;
|
|
627
|
+
// The artifact-first manual-failsafe invocation of the shared approval→save
|
|
628
|
+
// seam (the D1a gate exit lives in the seam). The drive-the-session fallback covers
|
|
629
|
+
// draft-LESS sessions — objectives have no transcript scrape by design, so a draftless
|
|
630
|
+
// session still needs a working save path.
|
|
631
|
+
const outcome = await objectiveApprovalSaveV1(pi, ctx, gating, { title });
|
|
632
|
+
// The manual save never consults the latch (it IS the deliberate retry) but reports into it.
|
|
633
|
+
if (outcome.status === "saved" || outcome.status === "save-failed")
|
|
634
|
+
recordSaveOutcome(reviews, "objective", {
|
|
635
|
+
confirmed: outcome.status === "saved",
|
|
636
|
+
...(outcome.result.details.ok ? {} : { detail: outcome.result.details.error }),
|
|
637
|
+
});
|
|
638
|
+
if (outcome.status === "refused-draft") {
|
|
639
|
+
// Fail-closed stop: the command's own precondition is a VALID draft — no gate exit,
|
|
640
|
+
// no driven turn (those fallbacks are for draft-LESS sessions; driving a fresh
|
|
641
|
+
// model-authored save over a corrupted artifact would silently abandon its bytes).
|
|
642
|
+
report(
|
|
643
|
+
ctx,
|
|
644
|
+
"objective-save",
|
|
645
|
+
"error",
|
|
646
|
+
`the working objective draft is invalid: ${outcome.problem} — rewrite it with ` +
|
|
647
|
+
"objective_draft, then re-run /objective-save",
|
|
648
|
+
);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (outcome.status === "no-draft") {
|
|
652
|
+
// Exit the read-only gate so the objective_save tool (excluded from READ_ONLY_TOOLS)
|
|
653
|
+
// becomes reachable on the driven turn, then drive the turn (mirrors /address and
|
|
654
|
+
// /objective-plan).
|
|
655
|
+
if (gating.isActive()) gating.exit(ctx);
|
|
656
|
+
report(ctx, "objective-save", "info", "handing the structured save to the session");
|
|
657
|
+
// The perk-objective-author pointer rides the skill-binding suffix (D5) since a
|
|
658
|
+
// warm /objective-save outside a stage:objective-author session gets none from Mechanism A.
|
|
659
|
+
pi.sendUserMessage(
|
|
660
|
+
objectiveSaveGuidance(title) + bindingSuffix(ctx.cwd, "stage:objective-author"),
|
|
661
|
+
);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
// Saved or save-failed: relay the save message. No node-link sub-step on the objective path,
|
|
665
|
+
// so the severity ladder is simpler than /plan-save's (no warning tier).
|
|
666
|
+
const result = outcome.result;
|
|
667
|
+
const message = result.content[0]?.text ?? "objective-save done";
|
|
668
|
+
const severity: Severity = result.details.ok ? "info" : "error";
|
|
669
|
+
report(ctx, "objective-save", severity, message);
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
}
|