@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
package/README.md
CHANGED
|
@@ -110,6 +110,11 @@ just ci
|
|
|
110
110
|
managed by [prek](https://prek.j178.dev) and runs `ruff check` on staged Python. Re-run
|
|
111
111
|
`just hooks` after a fresh clone if hooks are missing.
|
|
112
112
|
|
|
113
|
+
Focused Python selections while iterating — `just test-py-fast` (`-m "not slow"`) and
|
|
114
|
+
`just test-py-slow` (`-m slow`) — and how to time a tier are documented in
|
|
115
|
+
[docs/developers/testing.md](docs/developers/testing.md); `just test`, `just ci`, GitHub CI and
|
|
116
|
+
perk's in-session `run_ci` always run the whole Python suite.
|
|
117
|
+
|
|
113
118
|
Without `just`: `uv run …` for Python (`uv run perk init`, `uv run pytest`,
|
|
114
119
|
`uv run ruff check perk tests`, `uv run ty check`) and `npm run …` for TypeScript
|
|
115
120
|
(`npm run lint`, `npm run typecheck`).
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// The gist working-draft feature: the typed `GistDraft` model, its schema_version-1 JSON
|
|
2
|
+
// encode/decode (JSON is storage/transport only — the review surface is `renderGistDraft`'s
|
|
3
|
+
// markdown, never raw bytes), and the two draft operations over the WorkflowSession seam.
|
|
4
|
+
//
|
|
5
|
+
// Carve-out doctrine: the artifact name is the fixed constant `GIST_DRAFT_ARTIFACT` and every
|
|
6
|
+
// byte flows through the session seam (file + verified `session_artifacts` pointer), so the only
|
|
7
|
+
// thing the draft ops can ever touch is the one working-gist artifact in the current run's data
|
|
8
|
+
// dir (gitignored scratch). A revision is a WHOLE-VALUE replacement — no revision ids, no
|
|
9
|
+
// compare-and-swap claims the backing cannot prove.
|
|
10
|
+
//
|
|
11
|
+
// Format doctrine: the artifact carries `{schema_version, title?, scope?, prose}` — deliberately
|
|
12
|
+
// light: a gist is a problem-space statement of intent with no structured roadmap
|
|
13
|
+
// (contracts.md §8.41).
|
|
14
|
+
|
|
15
|
+
import type { SessionArtifactReceipt, WorkflowSession } from "../../session/workflowSession.ts";
|
|
16
|
+
|
|
17
|
+
/** The registry stage id of the gist-authoring session (shared with planMode's defer check). */
|
|
18
|
+
export const GIST_AUTHOR_STAGE = "gist-author";
|
|
19
|
+
|
|
20
|
+
/** The gist consumption tiers (`scope` — contracts.md §8.41). */
|
|
21
|
+
export const GIST_SCOPES = ["plan", "objective"] as const;
|
|
22
|
+
|
|
23
|
+
export type GistScope = (typeof GIST_SCOPES)[number];
|
|
24
|
+
|
|
25
|
+
/** The fixed working-gist artifact name (one JSON file: the prose + the optional scope hint). */
|
|
26
|
+
export const GIST_DRAFT_ARTIFACT = "gist-draft.json";
|
|
27
|
+
|
|
28
|
+
/** The validated working-gist draft shape. */
|
|
29
|
+
export interface GistDraft {
|
|
30
|
+
title?: string;
|
|
31
|
+
scope?: GistScope;
|
|
32
|
+
prose: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Serialize a working gist as the schema_version-1 JSON artifact: deterministic key order via
|
|
37
|
+
* the explicit literal; `title`/`scope` omitted when blank — byte-identical to what the artifact
|
|
38
|
+
* always carried. Pure; never throws.
|
|
39
|
+
*/
|
|
40
|
+
export function encodeGistDraft(draft: {
|
|
41
|
+
prose: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
scope?: GistScope;
|
|
44
|
+
}): string {
|
|
45
|
+
const title = draft.title?.trim();
|
|
46
|
+
const payload = {
|
|
47
|
+
schema_version: 1,
|
|
48
|
+
...(title ? { title } : {}),
|
|
49
|
+
...(draft.scope ? { scope: draft.scope } : {}),
|
|
50
|
+
prose: draft.prose,
|
|
51
|
+
};
|
|
52
|
+
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The classified decode outcome — `problem` carries the exact refusal bytes for edge rendering. */
|
|
56
|
+
export type DecodeGistDraftResult = { ok: true; draft: GistDraft } | { ok: false; problem: string };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Decode + validate working-gist artifact bytes. A CLASSIFIED refusal (never a warn+null
|
|
60
|
+
* fallback) on malformed JSON, a non-object payload, an unsupported `schema_version`, or blank
|
|
61
|
+
* prose — the same refusal taxonomy, with the problem bytes rendered by the consuming edge.
|
|
62
|
+
* `title` is kept only when a non-blank string; `scope` only when a member of the enum (an
|
|
63
|
+
* unknown scope degrades to absent, never poisons the draft). Never throws.
|
|
64
|
+
*/
|
|
65
|
+
export function decodeGistDraft(content: string): DecodeGistDraftResult {
|
|
66
|
+
const refuse = (why: string): { ok: false; problem: string } => ({
|
|
67
|
+
ok: false,
|
|
68
|
+
problem: `${GIST_DRAFT_ARTIFACT} ${why} — refusing the draft`,
|
|
69
|
+
});
|
|
70
|
+
let parsed: unknown;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(content);
|
|
73
|
+
} catch {
|
|
74
|
+
return refuse("is not valid JSON");
|
|
75
|
+
}
|
|
76
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
77
|
+
return refuse("is not a JSON object");
|
|
78
|
+
}
|
|
79
|
+
const payload = parsed as Record<string, unknown>;
|
|
80
|
+
if (payload.schema_version !== 1) {
|
|
81
|
+
return refuse(`has an unsupported schema_version (${JSON.stringify(payload.schema_version)})`);
|
|
82
|
+
}
|
|
83
|
+
const prose = payload.prose;
|
|
84
|
+
if (typeof prose !== "string" || !prose.trim()) {
|
|
85
|
+
return refuse("has no prose");
|
|
86
|
+
}
|
|
87
|
+
const title =
|
|
88
|
+
typeof payload.title === "string" && payload.title.trim() ? payload.title : undefined;
|
|
89
|
+
const scope =
|
|
90
|
+
typeof payload.scope === "string" && (GIST_SCOPES as readonly string[]).includes(payload.scope)
|
|
91
|
+
? (payload.scope as GistScope)
|
|
92
|
+
: undefined;
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
draft: {
|
|
96
|
+
...(title !== undefined ? { title } : {}),
|
|
97
|
+
...(scope !== undefined ? { scope } : {}),
|
|
98
|
+
prose,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render the draft as the markdown review surface (JSON is storage/transport only — contracts
|
|
105
|
+
* §8.1): the optional `# title` heading, a `Scope:` line when the hint is set, and the prose
|
|
106
|
+
* verbatim. Pure; never throws.
|
|
107
|
+
*/
|
|
108
|
+
export function renderGistDraft(draft: GistDraft): string {
|
|
109
|
+
let out = "";
|
|
110
|
+
if (draft.title) out += `# ${draft.title}\n\n`;
|
|
111
|
+
if (draft.scope) out += `Scope: ${draft.scope}\n\n`;
|
|
112
|
+
return out + draft.prose;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The revise outcome. `rejected` splits by `reason` so the adapter renders the exact failure
|
|
117
|
+
* taxonomy it always had: `blank_prose` (input refused), `no_identity` (no session), and
|
|
118
|
+
* `write_refused` (the seam refused before any effect); `unverified` means an effect may have
|
|
119
|
+
* landed but the read-back proof failed. `problem` carries the caller-facing message bytes.
|
|
120
|
+
*/
|
|
121
|
+
export type ReviseGistDraftResult =
|
|
122
|
+
| { status: "revised"; receipt: SessionArtifactReceipt; bytes: number }
|
|
123
|
+
| { status: "unchanged"; receipt: SessionArtifactReceipt; bytes: number }
|
|
124
|
+
| { status: "rejected"; reason: "blank_prose" | "no_identity" | "write_refused"; problem: string }
|
|
125
|
+
| { status: "unverified"; problem: string };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Rewrite the working gist draft (a whole-value replacement) through the session seam.
|
|
129
|
+
* Diagnostic precedence preserved: blank prose is refused FIRST, missing identity second
|
|
130
|
+
* (the identity-optional session classifies `runId: null` — an identity-less caller still
|
|
131
|
+
* opens), then the verified artifact write. Never throws.
|
|
132
|
+
*/
|
|
133
|
+
export function reviseGistDraft(
|
|
134
|
+
input: { prose: string; title?: string; scope?: GistScope },
|
|
135
|
+
session: WorkflowSession,
|
|
136
|
+
): ReviseGistDraftResult {
|
|
137
|
+
if (!input.prose.trim()) {
|
|
138
|
+
return {
|
|
139
|
+
status: "rejected",
|
|
140
|
+
reason: "blank_prose",
|
|
141
|
+
problem: "no gist prose to write (pass the full working draft)",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (session.runId === null) {
|
|
145
|
+
return {
|
|
146
|
+
status: "rejected",
|
|
147
|
+
reason: "no_identity",
|
|
148
|
+
problem: "session has no run_id — cannot write the gist-draft artifact",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const content = encodeGistDraft(input);
|
|
152
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
153
|
+
const written = session.writeArtifact(GIST_DRAFT_ARTIFACT, content);
|
|
154
|
+
switch (written.status) {
|
|
155
|
+
case "applied":
|
|
156
|
+
return { status: "revised", receipt: written.receipt, bytes };
|
|
157
|
+
case "unchanged":
|
|
158
|
+
return { status: "unchanged", receipt: written.receipt, bytes };
|
|
159
|
+
case "rejected":
|
|
160
|
+
return {
|
|
161
|
+
status: "rejected",
|
|
162
|
+
reason: "write_refused",
|
|
163
|
+
problem: `could not write the ${GIST_DRAFT_ARTIFACT} artifact (see warnings)`,
|
|
164
|
+
};
|
|
165
|
+
case "unverified":
|
|
166
|
+
return {
|
|
167
|
+
status: "unverified",
|
|
168
|
+
problem: `could not write the ${GIST_DRAFT_ARTIFACT} artifact (see warnings)`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The classified resume outcome: a refused draft is a fail-closed STOP at every consumer —
|
|
175
|
+
* it never takes the no-draft fallbacks' side effects (gate exit, driven turn). `raw` is the
|
|
176
|
+
* artifact bytes the draft was decoded from — the reviewed-bytes baseline; consumers that
|
|
177
|
+
* render and fence must use this one read for both (a second read could be newer than what
|
|
178
|
+
* the human saw).
|
|
179
|
+
*/
|
|
180
|
+
export type ResumeGistDraftResult =
|
|
181
|
+
| { kind: "valid"; draft: GistDraft; raw: string }
|
|
182
|
+
| { kind: "absent" }
|
|
183
|
+
| { kind: "refused"; problem: string };
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resume the working gist draft from the session, classified: seam `absent` → `absent` (the
|
|
187
|
+
* genuine no-draft arm); seam `invalid` → `refused` carrying the seam's problem (a corrupted
|
|
188
|
+
* artifact is truthfully rendered at the edge — the seam's own stderr tier is untouched); a
|
|
189
|
+
* decodable-but-refused payload → `refused` with the decoder's problem. Never throws.
|
|
190
|
+
*/
|
|
191
|
+
export function resumeGistDraft(session: WorkflowSession): ResumeGistDraftResult {
|
|
192
|
+
const read = session.readArtifact(GIST_DRAFT_ARTIFACT);
|
|
193
|
+
if (read.status === "absent") return { kind: "absent" };
|
|
194
|
+
if (read.status === "invalid") return { kind: "refused", problem: read.problem };
|
|
195
|
+
const decoded = decodeGistDraft(read.content);
|
|
196
|
+
if (!decoded.ok) return { kind: "refused", problem: decoded.problem };
|
|
197
|
+
return { kind: "valid", draft: decoded.draft, raw: read.content };
|
|
198
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The gist feature's model-facing prose units: the two tool-guideline arrays, the
|
|
2
|
+
// gist-authoring context content builder, and the injection marker/customType constants.
|
|
3
|
+
// Prose-unit MEANING and ORDER live here (the feature); the v1 adapter only PLACES these units
|
|
4
|
+
// in Pi registration fields and event payloads (module-contracts.md's prose split). Prompting,
|
|
5
|
+
// NOT enforcement — the read-only tool gate is the enforcement.
|
|
6
|
+
|
|
7
|
+
import { render } from "../../substrate/prompts.ts";
|
|
8
|
+
|
|
9
|
+
/** The gist-authoring context customType (distinct from planMode's `perk:plan-context`). */
|
|
10
|
+
export const GIST_AUTHOR_CONTEXT_TYPE = "perk:gist-author-context";
|
|
11
|
+
|
|
12
|
+
/** The injected gist-authoring context's identity marker (the strip + dedup key). */
|
|
13
|
+
export const GIST_AUTHOR_MARKER = "[GIST AUTHORING]";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The gist-authoring session context: live state + pointers only (contracts.md §8.57 — the flow
|
|
17
|
+
* is stated by the launch statement, the detail by the `perk-gist-author` skill). It names the
|
|
18
|
+
* working-draft artifact (`gist_draft`), the review tool (`plan_review`), and the bound skill;
|
|
19
|
+
* it never restates the flow.
|
|
20
|
+
*/
|
|
21
|
+
export const GIST_AUTHORING_CONTEXT = render("contexts/gist-authoring.md", {
|
|
22
|
+
marker: GIST_AUTHOR_MARKER,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build the full gist-authoring injection, appending the project-config authoring addendum when
|
|
27
|
+
* present. Pure over the addendum — the ADAPTER loads `[workflow] plan_authoring` per event and
|
|
28
|
+
* passes the value in (the narrow-views doctrine).
|
|
29
|
+
*/
|
|
30
|
+
export function gistAuthoringContextContent(addendum: string | undefined): string {
|
|
31
|
+
return addendum ? `${GIST_AUTHORING_CONTEXT}\n\n${addendum.trim()}` : GIST_AUTHORING_CONTEXT;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The `gist_draft` tool guidelines (verbatim prose units; the adapter places them). */
|
|
35
|
+
export const GIST_DRAFT_TOOL_GUIDELINES = [
|
|
36
|
+
"Call gist_draft to persist the current working gist as you author or revise it; pass the FULL prose each time (it rewrites the whole draft).",
|
|
37
|
+
"gist_draft never saves to the issue backend and never ends the turn — gist_save//gist-save remain the canonical save surface.",
|
|
38
|
+
"Pass gist_draft's `scope` only once the consumption tier is settled: `plan` for plan-sized intent, `objective` for objective-sized intent.",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** The `gist_save` tool guidelines (verbatim prose units; the adapter places them). */
|
|
42
|
+
export const GIST_SAVE_TOOL_GUIDELINES = [
|
|
43
|
+
"Use gist_save only after the gist says what it means; it creates the tracked gist in the issue backend and ends the turn.",
|
|
44
|
+
"Pass gist_save the statement-of-intent PROSE in `prose` — problem-focused, with at most high-level solution leanings; no implementation steps or roadmap.",
|
|
45
|
+
"Pass gist_save's `scope` only once the consumption tier is settled (plan or objective); omit it to keep the pre-seeded/default scope.",
|
|
46
|
+
];
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// The gist review feature: the `GistDraftReviewer` role (one production adapter per review
|
|
2
|
+
// backend — plannotator bridge or first-party editor, both built in pi/v1 — plus a scripted fake
|
|
3
|
+
// in the tests) and the one-entry `reviewGist` operation.
|
|
4
|
+
//
|
|
5
|
+
// Ordering is the review door's exact discipline: resume-the-draft FIRST (absent → `noDraft` —
|
|
6
|
+
// the draft artifact is the SOLE review source, never a param, never the transcript), render the
|
|
7
|
+
// markdown surface, review, then route the verdict — an approval carrying direct edits SKIPS the
|
|
8
|
+
// save and returns one revise round (rendered edits cannot be folded back into the structured
|
|
9
|
+
// draft mechanically; the gate stays untouched); a plain approval re-reads the artifact through
|
|
10
|
+
// `gistApprovalSave` (the save source is the artifact, never the reviewed bytes). Headless
|
|
11
|
+
// detection stays in the adapter — the feature never sees `hasUI`.
|
|
12
|
+
|
|
13
|
+
import type { WorkflowSession } from "../../session/workflowSession.ts";
|
|
14
|
+
import type { ApprovalGate } from "../review/approvalGate.ts";
|
|
15
|
+
import { renderGistDraft, resumeGistDraft } from "./draft.ts";
|
|
16
|
+
import { type GistApprovalSaveOutcome, type GistBackend, gistApprovalSave } from "./save.ts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The reviewer's verdict on the rendered draft. An approval carrying reviewer edits of the
|
|
20
|
+
* rendered markdown is its OWN variant (`approvedDirectEdits`) with `feedback` required — the
|
|
21
|
+
* edits ARE the feedback, so an edits-without-feedback value is unrepresentable and can never
|
|
22
|
+
* fall through to the save path. The backend adapter translates its own vocabulary (e.g.
|
|
23
|
+
* plannotator's `# Direct Edits` feedback section) into this variant.
|
|
24
|
+
*/
|
|
25
|
+
export type GistReviewOutcome =
|
|
26
|
+
| { status: "approved"; feedback?: string; reviewId?: string }
|
|
27
|
+
| { status: "approvedDirectEdits"; feedback: string; reviewId?: string }
|
|
28
|
+
| { status: "denied"; feedback?: string; reviewId?: string }
|
|
29
|
+
| { status: "dismissed" }
|
|
30
|
+
| { status: "aborted" }
|
|
31
|
+
| { status: "unavailable"; warning: string };
|
|
32
|
+
|
|
33
|
+
/** The reviewer role: judge the RENDERED markdown surface (never raw artifact JSON). */
|
|
34
|
+
export interface GistDraftReviewer {
|
|
35
|
+
review(rendered: string, signal?: AbortSignal): Promise<GistReviewOutcome>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The one-entry review outcome — each arm carries exactly what its caller renders.
|
|
39
|
+
* `refusedDraft` (pre-review) and `approvedRefusedDraft` (the approval-time race) are the
|
|
40
|
+
* fail-closed stops for an invalid artifact: nothing reviewed/saved, the gate untouched. */
|
|
41
|
+
export type ReviewGistResult =
|
|
42
|
+
| { status: "noDraft" }
|
|
43
|
+
| { status: "refusedDraft"; problem: string }
|
|
44
|
+
| { status: "directEditsRevise"; feedback: string; reviewId?: string }
|
|
45
|
+
| {
|
|
46
|
+
status: "approvedSaved";
|
|
47
|
+
save: Extract<GistApprovalSaveOutcome, { status: "saved" }>;
|
|
48
|
+
feedback?: string;
|
|
49
|
+
reviewId?: string;
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
status: "approvedSaveFailed";
|
|
53
|
+
save: Extract<GistApprovalSaveOutcome, { status: "save-failed" }>;
|
|
54
|
+
feedback?: string;
|
|
55
|
+
reviewId?: string;
|
|
56
|
+
}
|
|
57
|
+
| { status: "approvedNoDraft"; feedback?: string; reviewId?: string }
|
|
58
|
+
| { status: "approvedRefusedDraft"; problem: string; feedback?: 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 gist draft end-to-end: resume → render → review → route. An approval with
|
|
66
|
+
* `directEdits` (and feedback to fold) returns the revise round with NOTHING saved and the gate
|
|
67
|
+
* untouched; a plain approval runs `gistApprovalSave` (which re-reads the artifact at save time
|
|
68
|
+
* — `approvedNoDraft` is the defensive vanished-between-reads arm; `approvedRefusedDraft` its
|
|
69
|
+
* corrupted-between-reads sibling, feedback/reviewId preserved across the race). Never throws.
|
|
70
|
+
*/
|
|
71
|
+
export async function reviewGist(
|
|
72
|
+
deps: {
|
|
73
|
+
session: WorkflowSession;
|
|
74
|
+
reviewer: GistDraftReviewer;
|
|
75
|
+
backend: GistBackend;
|
|
76
|
+
gate: ApprovalGate;
|
|
77
|
+
},
|
|
78
|
+
signal?: AbortSignal,
|
|
79
|
+
): Promise<ReviewGistResult> {
|
|
80
|
+
if (signal?.aborted) return { status: "aborted" };
|
|
81
|
+
const resumed = resumeGistDraft(deps.session);
|
|
82
|
+
if (resumed.kind === "absent") return { status: "noDraft" };
|
|
83
|
+
if (resumed.kind === "refused") return { status: "refusedDraft", problem: resumed.problem };
|
|
84
|
+
const rendered = renderGistDraft(resumed.draft);
|
|
85
|
+
const outcome = await deps.reviewer.review(rendered, signal);
|
|
86
|
+
if (signal?.aborted) return { status: "aborted" };
|
|
87
|
+
return completeGistReview(outcome, () => gistApprovalSave(deps));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Subject policy only: callers authorize effects before entering this completion seam. */
|
|
91
|
+
export async function completeGistReview(
|
|
92
|
+
outcome: GistReviewOutcome,
|
|
93
|
+
approvalSave: () => Promise<GistApprovalSaveOutcome>,
|
|
94
|
+
): Promise<ReviewGistResult> {
|
|
95
|
+
if (outcome.status === "approvedDirectEdits") {
|
|
96
|
+
return {
|
|
97
|
+
status: "directEditsRevise",
|
|
98
|
+
feedback: outcome.feedback,
|
|
99
|
+
...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (outcome.status === "approved") {
|
|
103
|
+
const carried = {
|
|
104
|
+
...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
|
|
105
|
+
...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
|
|
106
|
+
};
|
|
107
|
+
const save = await approvalSave();
|
|
108
|
+
switch (save.status) {
|
|
109
|
+
case "saved":
|
|
110
|
+
return { status: "approvedSaved", save, ...carried };
|
|
111
|
+
case "save-failed":
|
|
112
|
+
return { status: "approvedSaveFailed", save, ...carried };
|
|
113
|
+
case "no-draft":
|
|
114
|
+
return { status: "approvedNoDraft", ...carried };
|
|
115
|
+
case "refused-draft":
|
|
116
|
+
return { status: "approvedRefusedDraft", problem: save.problem, ...carried };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
switch (outcome.status) {
|
|
120
|
+
case "denied":
|
|
121
|
+
return {
|
|
122
|
+
status: "denied",
|
|
123
|
+
...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
|
|
124
|
+
...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
|
|
125
|
+
};
|
|
126
|
+
case "dismissed":
|
|
127
|
+
return { status: "dismissed" };
|
|
128
|
+
case "aborted":
|
|
129
|
+
return { status: "aborted" };
|
|
130
|
+
case "unavailable":
|
|
131
|
+
return { status: "unavailable", warning: outcome.warning };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// The gist save feature: the narrow exterior `GistBackend` port (one production adapter — the
|
|
2
|
+
// `perk gist create` cold door in pi/v1 — plus one deterministic fake in the tests: the port
|
|
3
|
+
// admission rule), the `saveGist` operation, and the shared APPROVED-review → save orchestration
|
|
4
|
+
// `gistApprovalSave` (the exported name contracts.md §8.41 pins).
|
|
5
|
+
//
|
|
6
|
+
// Unlike the plan/objective siblings there is NO session linkage after the save — nothing
|
|
7
|
+
// consumes a gist in-session (adoption happens later via the cold doors), so a successful save
|
|
8
|
+
// just carries the backend's id/url/scope facts for the caller to relay.
|
|
9
|
+
|
|
10
|
+
import type { WorkflowSession } from "../../session/workflowSession.ts";
|
|
11
|
+
import { type ApprovalGate, saveThroughApprovalGate } from "../review/approvalGate.ts";
|
|
12
|
+
import { GIST_SCOPES, type GistScope, resumeGistDraft } from "./draft.ts";
|
|
13
|
+
|
|
14
|
+
/** The backend save facts (`id` is the opaque string gist id — contracts §8.21). */
|
|
15
|
+
export type GistBackendSaveResult =
|
|
16
|
+
| { status: "saved"; id: string; url: string; existed: boolean | null; scope: string | null }
|
|
17
|
+
| { status: "failed"; message: string; errorType: string };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The narrow exterior port the save operation writes through. `runId: null` means the caller has
|
|
21
|
+
* no session identity — the backend omits its run linkage (an identity-less save keeps working).
|
|
22
|
+
*/
|
|
23
|
+
export interface GistBackend {
|
|
24
|
+
save(req: {
|
|
25
|
+
prose: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
scope?: GistScope;
|
|
28
|
+
runId: string | null;
|
|
29
|
+
}): Promise<GistBackendSaveResult>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The save outcome: the backend facts, or a typed refusal/failure (message bytes caller-facing).
|
|
34
|
+
* Deliberately the SAME union as the backend result — `saveGist` adds the local validation
|
|
35
|
+
* refusals on the shared `failed` variant and otherwise returns the backend value unchanged, so
|
|
36
|
+
* a second hand-mirrored vocabulary would only invite drift. Alias, not copy.
|
|
37
|
+
*/
|
|
38
|
+
export type SaveGistOutcome = GistBackendSaveResult;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The single save operation both surfaces call: validate the prose/scope (the exact refusals the
|
|
42
|
+
* save always had), then write through the backend port. The backend owns scope resolution
|
|
43
|
+
* beyond the explicit value (launch-handoff seed, default). Never throws.
|
|
44
|
+
*/
|
|
45
|
+
export async function saveGist(
|
|
46
|
+
input: { prose: string; title?: string; scope?: GistScope },
|
|
47
|
+
deps: { backend: GistBackend; runId: string | null },
|
|
48
|
+
): Promise<SaveGistOutcome> {
|
|
49
|
+
const prose = input.prose.trim();
|
|
50
|
+
if (!prose) {
|
|
51
|
+
return {
|
|
52
|
+
status: "failed",
|
|
53
|
+
message: "no gist prose to save (draft the gist first)",
|
|
54
|
+
errorType: "invalid_input",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (input.scope !== undefined && !(GIST_SCOPES as readonly string[]).includes(input.scope)) {
|
|
58
|
+
return {
|
|
59
|
+
status: "failed",
|
|
60
|
+
message: "scope must be plan or objective",
|
|
61
|
+
errorType: "invalid_input",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return deps.backend.save({
|
|
65
|
+
prose,
|
|
66
|
+
...(input.title !== undefined ? { title: input.title } : {}),
|
|
67
|
+
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
68
|
+
runId: deps.runId,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The approval→save orchestration outcome (the gist `ApprovalSaveOutcome`). `refused-draft`
|
|
73
|
+
* is the fail-closed stop for an invalid artifact: nothing saved, the gate never touched —
|
|
74
|
+
* distinct from `no-draft` (the genuine draft-less fallback arm). */
|
|
75
|
+
export type GistApprovalSaveOutcome =
|
|
76
|
+
| { status: "no-draft" }
|
|
77
|
+
| { status: "refused-draft"; problem: string }
|
|
78
|
+
| { status: "saved"; save: Extract<SaveGistOutcome, { status: "saved" }>; gateExited: boolean }
|
|
79
|
+
| {
|
|
80
|
+
status: "save-failed";
|
|
81
|
+
save: Extract<SaveGistOutcome, { status: "failed" }>;
|
|
82
|
+
gateExited: false;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The shared APPROVED-review → save orchestration (an APPROVED `plan_review` gist arm and the
|
|
87
|
+
* manual `/gist-save` failsafe both run THIS). Flow: re-read the draft artifact at save time
|
|
88
|
+
* (`resumeGistDraft` — never the rendered markdown, never in-hand bytes) → `saveGist` through
|
|
89
|
+
* `saveThroughApprovalGate` (the D1a invariant: snapshot before the save; exit only after a
|
|
90
|
+
* successful save while read-only; a failed save leaves the gate ON). No draft → `no-draft`
|
|
91
|
+
* (nothing saved, the gate untouched); a REFUSED draft → `refused-draft` before the gate
|
|
92
|
+
* snapshot (fail-closed stop — `gateExited` semantics never arise). Title precedence:
|
|
93
|
+
* the explicit override (`/gist-save [title]` — a pinned behavior) wins over the draft's; scope
|
|
94
|
+
* is always the draft's.
|
|
95
|
+
*/
|
|
96
|
+
export async function gistApprovalSave(
|
|
97
|
+
deps: { session: WorkflowSession; backend: GistBackend; gate: ApprovalGate },
|
|
98
|
+
opts: { title?: string } = {},
|
|
99
|
+
): Promise<GistApprovalSaveOutcome> {
|
|
100
|
+
const resumed = resumeGistDraft(deps.session);
|
|
101
|
+
if (resumed.kind === "absent") return { status: "no-draft" };
|
|
102
|
+
if (resumed.kind === "refused") return { status: "refused-draft", problem: resumed.problem };
|
|
103
|
+
const draft = resumed.draft;
|
|
104
|
+
const { outcome: save, gateExited } = await saveThroughApprovalGate(deps.gate, () =>
|
|
105
|
+
saveGist(
|
|
106
|
+
{
|
|
107
|
+
prose: draft.prose,
|
|
108
|
+
...(opts.title !== undefined || draft.title !== undefined
|
|
109
|
+
? { title: opts.title ?? draft.title }
|
|
110
|
+
: {}),
|
|
111
|
+
...(draft.scope !== undefined ? { scope: draft.scope } : {}),
|
|
112
|
+
},
|
|
113
|
+
{ backend: deps.backend, runId: deps.session.runId },
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
if (save.status !== "saved") return { status: "save-failed", save, gateExited: false };
|
|
117
|
+
return { status: "saved", save, gateExited };
|
|
118
|
+
}
|