implannotator 0.2.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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/docs/architecture.md +12 -0
- package/docs/compatibility.md +9 -0
- package/docs/configuration.md +25 -0
- package/docs/installation.md +30 -0
- package/docs/qa.md +5 -0
- package/docs/security.md +10 -0
- package/docs/troubleshooting.md +17 -0
- package/docs/workflow.md +12 -0
- package/extensions/commands/register.ts +39 -0
- package/extensions/config/defaults.ts +16 -0
- package/extensions/config/loader.ts +84 -0
- package/extensions/domain/types.ts +94 -0
- package/extensions/index.ts +44 -0
- package/extensions/plannotator/availability.ts +13 -0
- package/extensions/plannotator/client.ts +54 -0
- package/extensions/plannotator/contracts.ts +20 -0
- package/extensions/policy/change-inventory.ts +23 -0
- package/extensions/policy/mutation-gate.ts +40 -0
- package/extensions/policy/non-git-diff.ts +29 -0
- package/extensions/qa/checklist.ts +31 -0
- package/extensions/qa/classifier.ts +23 -0
- package/extensions/review/final-report.ts +6 -0
- package/extensions/state/atomic-file.ts +14 -0
- package/extensions/state/redaction.ts +19 -0
- package/extensions/state/run-layout.ts +40 -0
- package/extensions/state/run-store.ts +97 -0
- package/extensions/tools/control-tool.ts +95 -0
- package/extensions/workflow/approval-policy.ts +21 -0
- package/extensions/workflow/orchestrator.ts +290 -0
- package/extensions/workflow/transitions.ts +26 -0
- package/package.json +77 -0
- package/schemas/config.schema.json +63 -0
- package/schemas/decision.schema.json +16 -0
- package/schemas/qa-report.schema.json +14 -0
- package/schemas/run.schema.json +15 -0
- package/scripts/assert-publish-ready.mjs +6 -0
- package/scripts/check-plannotator-compat.mjs +23 -0
- package/scripts/update-check.mjs +32 -0
- package/scripts/verify-implannotator-snapshot.mjs +24 -0
- package/skills/implannotator/SKILL.md +194 -0
- package/skills/implannotator/agents/implannotator_asset_producer.toml +92 -0
- package/skills/implannotator/agents/implannotator_manual_edit_applier.toml +95 -0
- package/skills/implannotator/agents/openai.yaml +4 -0
- package/skills/implannotator/reference/adapt.md +311 -0
- package/skills/implannotator/reference/animate.md +201 -0
- package/skills/implannotator/reference/approval.md +26 -0
- package/skills/implannotator/reference/audit.md +133 -0
- package/skills/implannotator/reference/bolder.md +113 -0
- package/skills/implannotator/reference/brand.md +108 -0
- package/skills/implannotator/reference/clarify.md +288 -0
- package/skills/implannotator/reference/codex.md +105 -0
- package/skills/implannotator/reference/colorize.md +257 -0
- package/skills/implannotator/reference/craft.md +123 -0
- package/skills/implannotator/reference/critique.md +790 -0
- package/skills/implannotator/reference/delight.md +302 -0
- package/skills/implannotator/reference/distill.md +111 -0
- package/skills/implannotator/reference/document.md +429 -0
- package/skills/implannotator/reference/extract.md +69 -0
- package/skills/implannotator/reference/harden.md +347 -0
- package/skills/implannotator/reference/hooks.md +90 -0
- package/skills/implannotator/reference/implannotator-workflow.md +65 -0
- package/skills/implannotator/reference/init.md +172 -0
- package/skills/implannotator/reference/interaction-design.md +189 -0
- package/skills/implannotator/reference/layout.md +161 -0
- package/skills/implannotator/reference/live.md +720 -0
- package/skills/implannotator/reference/onboard.md +234 -0
- package/skills/implannotator/reference/optimize.md +258 -0
- package/skills/implannotator/reference/overdrive.md +130 -0
- package/skills/implannotator/reference/polish.md +241 -0
- package/skills/implannotator/reference/product.md +60 -0
- package/skills/implannotator/reference/qa.md +40 -0
- package/skills/implannotator/reference/quieter.md +99 -0
- package/skills/implannotator/reference/review-repair.md +10 -0
- package/skills/implannotator/reference/shape.md +165 -0
- package/skills/implannotator/reference/typeset.md +279 -0
- package/skills/implannotator/scripts/command-metadata.json +94 -0
- package/skills/implannotator/scripts/context-signals.mjs +225 -0
- package/skills/implannotator/scripts/context.mjs +280 -0
- package/skills/implannotator/scripts/critique-storage.mjs +242 -0
- package/skills/implannotator/scripts/detect-csp.mjs +198 -0
- package/skills/implannotator/scripts/detect.mjs +21 -0
- package/skills/implannotator/scripts/detector/browser/injected/index.mjs +1937 -0
- package/skills/implannotator/scripts/detector/cli/main.mjs +268 -0
- package/skills/implannotator/scripts/detector/design-system.mjs +750 -0
- package/skills/implannotator/scripts/detector/detect-antipatterns-browser.js +5138 -0
- package/skills/implannotator/scripts/detector/detect-antipatterns.mjs +50 -0
- package/skills/implannotator/scripts/detector/engines/browser/detect-url.mjs +277 -0
- package/skills/implannotator/scripts/detector/engines/regex/detect-text.mjs +564 -0
- package/skills/implannotator/scripts/detector/engines/static-html/css-cascade.mjs +1015 -0
- package/skills/implannotator/scripts/detector/engines/static-html/detect-html.mjs +229 -0
- package/skills/implannotator/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
- package/skills/implannotator/scripts/detector/findings.mjs +12 -0
- package/skills/implannotator/scripts/detector/node/file-system.mjs +198 -0
- package/skills/implannotator/scripts/detector/profile/profiler.mjs +166 -0
- package/skills/implannotator/scripts/detector/registry/antipatterns.mjs +448 -0
- package/skills/implannotator/scripts/detector/rules/checks.mjs +2671 -0
- package/skills/implannotator/scripts/detector/shared/color.mjs +124 -0
- package/skills/implannotator/scripts/detector/shared/constants.mjs +101 -0
- package/skills/implannotator/scripts/detector/shared/page.mjs +7 -0
- package/skills/implannotator/scripts/hook-admin.mjs +636 -0
- package/skills/implannotator/scripts/hook-before-edit.mjs +476 -0
- package/skills/implannotator/scripts/hook-lib.mjs +1526 -0
- package/skills/implannotator/scripts/hook.mjs +61 -0
- package/skills/implannotator/scripts/lib/design-parser.mjs +842 -0
- package/skills/implannotator/scripts/lib/implannotator-config.mjs +638 -0
- package/skills/implannotator/scripts/lib/implannotator-paths.mjs +126 -0
- package/skills/implannotator/scripts/lib/is-generated.mjs +69 -0
- package/skills/implannotator/scripts/live/browser-script-parts.mjs +49 -0
- package/skills/implannotator/scripts/live/completion.mjs +19 -0
- package/skills/implannotator/scripts/live/event-validation.mjs +137 -0
- package/skills/implannotator/scripts/live/insert-ui.mjs +458 -0
- package/skills/implannotator/scripts/live/manual-apply.mjs +939 -0
- package/skills/implannotator/scripts/live/manual-edit-routes.mjs +357 -0
- package/skills/implannotator/scripts/live/manual-edits-buffer.mjs +152 -0
- package/skills/implannotator/scripts/live/session-store.mjs +289 -0
- package/skills/implannotator/scripts/live/svelte-component.mjs +826 -0
- package/skills/implannotator/scripts/live/sveltekit-adapter.mjs +274 -0
- package/skills/implannotator/scripts/live/ui-core.mjs +180 -0
- package/skills/implannotator/scripts/live/vocabulary.mjs +36 -0
- package/skills/implannotator/scripts/live-accept.mjs +812 -0
- package/skills/implannotator/scripts/live-browser-dom.js +146 -0
- package/skills/implannotator/scripts/live-browser-session.js +123 -0
- package/skills/implannotator/scripts/live-browser.js +11161 -0
- package/skills/implannotator/scripts/live-commit-manual-edits.mjs +1241 -0
- package/skills/implannotator/scripts/live-complete.mjs +75 -0
- package/skills/implannotator/scripts/live-copy-edit-agent.mjs +683 -0
- package/skills/implannotator/scripts/live-discard-manual-edits.mjs +51 -0
- package/skills/implannotator/scripts/live-inject.mjs +583 -0
- package/skills/implannotator/scripts/live-insert.mjs +272 -0
- package/skills/implannotator/scripts/live-manual-edit-evidence.mjs +363 -0
- package/skills/implannotator/scripts/live-poll.mjs +379 -0
- package/skills/implannotator/scripts/live-resume.mjs +94 -0
- package/skills/implannotator/scripts/live-server.mjs +1134 -0
- package/skills/implannotator/scripts/live-status.mjs +61 -0
- package/skills/implannotator/scripts/live-wrap.mjs +894 -0
- package/skills/implannotator/scripts/live.mjs +246 -0
- package/skills/implannotator/scripts/modern-screenshot.umd.js +14 -0
- package/skills/implannotator/scripts/palette.mjs +633 -0
- package/skills/implannotator/scripts/pin.mjs +214 -0
- package/skills/implannotator/vendor/impeccable-manifest.json +680 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ImplannotatorConfig, QaProfile, QaReport, ReviewRecord, ReviewStage, RiskLevel, RunRecord } from "../domain/types.js";
|
|
3
|
+
import { baselineFiles, changedFiles } from "../policy/change-inventory.js";
|
|
4
|
+
import { PlannotatorClient } from "../plannotator/client.js";
|
|
5
|
+
import { finalReport } from "../review/final-report.js";
|
|
6
|
+
import { RunStore } from "../state/run-store.js";
|
|
7
|
+
import { chooseAfterRejection } from "./approval-policy.js";
|
|
8
|
+
import { transition } from "./transitions.js";
|
|
9
|
+
|
|
10
|
+
export interface WorkflowResult {
|
|
11
|
+
run: RunRecord;
|
|
12
|
+
message: string;
|
|
13
|
+
needsTextDecision?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function now(): string { return new Date().toISOString(); }
|
|
17
|
+
|
|
18
|
+
const SAFE_RESUME_PHASES = new Set<RunRecord["phase"]>([
|
|
19
|
+
"inspecting", "planning", "awaiting-plan-review", "awaiting-text-approval", "approved", "qa", "initial-review", "final-review",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export class Orchestrator {
|
|
23
|
+
readonly store: RunStore;
|
|
24
|
+
readonly client: PlannotatorClient;
|
|
25
|
+
|
|
26
|
+
constructor(readonly pi: ExtensionAPI, readonly cwd: string, readonly config: ImplannotatorConfig) {
|
|
27
|
+
this.store = new RunStore(cwd, config.audit.directory);
|
|
28
|
+
this.client = new PlannotatorClient(pi);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
active(): RunRecord | undefined { return this.store.active(); }
|
|
32
|
+
|
|
33
|
+
start(command: string, target: string | undefined, risk: RiskLevel, qaProfile: QaProfile): WorkflowResult {
|
|
34
|
+
const existing = this.active();
|
|
35
|
+
if (existing && !["complete", "stopped", "failed"].includes(existing.phase)) {
|
|
36
|
+
throw new Error(`Run ${existing.id} is already active in phase ${existing.phase}. Resume or stop it first.`);
|
|
37
|
+
}
|
|
38
|
+
const run = this.store.create(command, target, risk, qaProfile);
|
|
39
|
+
const baseline = baselineFiles(this.cwd);
|
|
40
|
+
if (baseline) run.baselineFiles = baseline;
|
|
41
|
+
this.store.appendDecision(run, { at: now(), kind: "run", actor: "user", decision: "started", reason: `${command}${target ? ` ${target}` : ""}` });
|
|
42
|
+
return { run, message: `Run ${run.id} started. Inspect the project without editing, then submit a complete plan.` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async submitPlan(plan: string, ctx: ExtensionContext, signal?: AbortSignal): Promise<WorkflowResult> {
|
|
46
|
+
const run = this.requireActive();
|
|
47
|
+
if (run.phase === "inspecting") run.phase = transition(run.phase, "planning");
|
|
48
|
+
if (run.phase !== "planning") throw new Error(`Cannot submit a plan during ${run.phase}.`);
|
|
49
|
+
run.plan = plan;
|
|
50
|
+
|
|
51
|
+
if (this.config.approval.allowLowRiskAutoApprove && run.risk === "low" && !ctx.hasUI) {
|
|
52
|
+
run.phase = transition(run.phase, "approved");
|
|
53
|
+
this.store.appendDecision(run, { at: now(), kind: "plan", actor: "system", decision: "auto-approved", reason: "Explicit low-risk fallback setting enabled." });
|
|
54
|
+
return { run, message: "Plan auto-approved by explicit low-risk fallback configuration. Begin implementation." };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
run.phase = transition(run.phase, "awaiting-plan-review");
|
|
58
|
+
this.store.save(run);
|
|
59
|
+
const response = await this.client.startPlanReview(plan, signal);
|
|
60
|
+
if (response.status === "handled") {
|
|
61
|
+
run.planReviewId = response.result.reviewId;
|
|
62
|
+
this.store.save(run);
|
|
63
|
+
const result = await this.client.waitForPlanDecision(response.result.reviewId, this.config.approval.browserTimeoutMs, signal);
|
|
64
|
+
if (result.status === "completed") {
|
|
65
|
+
if (result.approved) {
|
|
66
|
+
run.phase = transition(run.phase, "approved");
|
|
67
|
+
this.store.appendDecision(run, { at: now(), kind: "plan", actor: "user", decision: "approved", ...(result.feedback ? { reason: result.feedback } : {}) });
|
|
68
|
+
return { run, message: "Plan approved in Plannotator. Begin implementation." };
|
|
69
|
+
}
|
|
70
|
+
return this.handlePlanRejection(run, ctx, result.feedback);
|
|
71
|
+
}
|
|
72
|
+
run.phase = transition(run.phase, "awaiting-text-approval");
|
|
73
|
+
run.pendingTextReview = "plan";
|
|
74
|
+
this.store.save(run);
|
|
75
|
+
return { run, message: "The browser review could not be recovered. Ask the user to approve, revise, or stop in chat.", needsTextDecision: true };
|
|
76
|
+
}
|
|
77
|
+
return this.textPlanFallback(run, ctx, response.error);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async textPlanFallback(run: RunRecord, ctx: ExtensionContext, reason?: string): Promise<WorkflowResult> {
|
|
81
|
+
if (ctx.hasUI) {
|
|
82
|
+
const edited = await ctx.ui.editor("Implannotator text plan review", run.plan ?? "");
|
|
83
|
+
if (edited !== undefined && edited.trim() && edited !== run.plan) run.plan = edited;
|
|
84
|
+
const choice = await ctx.ui.select("Approve implementation plan?", ["Approve", "Revise", "Stop"]);
|
|
85
|
+
if (choice === "Approve") {
|
|
86
|
+
run.phase = transition(run.phase, "approved");
|
|
87
|
+
this.store.appendDecision(run, { at: now(), kind: "plan", actor: "user", decision: "approved-text-fallback", ...(reason ? { reason } : {}) });
|
|
88
|
+
return { run, message: "Plan approved through text fallback. Begin implementation." };
|
|
89
|
+
}
|
|
90
|
+
if (choice === "Revise") {
|
|
91
|
+
run.phase = transition(run.phase, "planning");
|
|
92
|
+
this.store.appendDecision(run, { at: now(), kind: "plan", actor: "user", decision: "revise", ...(reason ? { reason } : {}) });
|
|
93
|
+
return { run, message: "Revise the plan and submit it again." };
|
|
94
|
+
}
|
|
95
|
+
run.phase = transition(run.phase, "awaiting-text-approval");
|
|
96
|
+
run.pendingTextReview = "plan";
|
|
97
|
+
this.pause(run);
|
|
98
|
+
this.store.appendDecision(run, { at: now(), kind: "run", actor: "user", decision: "stopped", ...(reason ? { reason } : {}) });
|
|
99
|
+
return { run, message: "Run stopped." };
|
|
100
|
+
}
|
|
101
|
+
run.phase = transition(run.phase, "awaiting-text-approval");
|
|
102
|
+
run.pendingTextReview = "plan";
|
|
103
|
+
this.store.save(run);
|
|
104
|
+
return { run, message: `Browser review unavailable${reason ? `: ${reason}` : ""}. Present the plan in chat and ask the user to approve, revise, proceed with warning, or stop.`, needsTextDecision: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async handlePlanRejection(run: RunRecord, ctx: ExtensionContext, feedback?: string): Promise<WorkflowResult> {
|
|
108
|
+
const choice = await chooseAfterRejection(ctx, run, feedback);
|
|
109
|
+
if (choice === "text-required") {
|
|
110
|
+
run.phase = transition(run.phase, "awaiting-text-approval");
|
|
111
|
+
run.pendingTextReview = "plan";
|
|
112
|
+
this.store.save(run);
|
|
113
|
+
return { run, message: `Plan was not approved${feedback ? `: ${feedback}` : ""}. Ask the user to revise, proceed anyway, or stop.`, needsTextDecision: true };
|
|
114
|
+
}
|
|
115
|
+
if (choice === "proceed") {
|
|
116
|
+
run.phase = transition(run.phase, "approved");
|
|
117
|
+
run.completionOverride = true;
|
|
118
|
+
this.store.appendDecision(run, { at: now(), kind: "plan", actor: "user", decision: "proceed-anyway", reason: feedback ?? "Plan review rejected or closed.", warned: true });
|
|
119
|
+
return { run, message: "Approval override recorded. Begin implementation." };
|
|
120
|
+
}
|
|
121
|
+
if (choice === "revise") {
|
|
122
|
+
run.phase = transition(run.phase, "planning");
|
|
123
|
+
this.store.appendDecision(run, { at: now(), kind: "plan", actor: "user", decision: "revise", ...(feedback ? { reason: feedback } : {}) });
|
|
124
|
+
return { run, message: `Revise and resubmit the plan${feedback ? ` using this feedback: ${feedback}` : "."}` };
|
|
125
|
+
}
|
|
126
|
+
run.phase = transition(run.phase, "awaiting-text-approval");
|
|
127
|
+
run.pendingTextReview = "plan";
|
|
128
|
+
this.pause(run);
|
|
129
|
+
this.store.appendDecision(run, { at: now(), kind: "run", actor: "user", decision: "stopped-after-plan-review" });
|
|
130
|
+
return { run, message: "Run stopped." };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
textDecision(stage: ReviewStage, decision: "approve" | "revise" | "proceed" | "stop", reason?: string, warned = false, confirmation?: string): WorkflowResult {
|
|
134
|
+
const run = this.requireActive();
|
|
135
|
+
if (run.phase !== "awaiting-text-approval" || run.pendingTextReview !== stage) throw new Error(`No ${stage} text decision is pending.`);
|
|
136
|
+
if (decision === "proceed" && (!warned || confirmation !== "PROCEED WITHOUT APPROVAL")) {
|
|
137
|
+
throw new Error('Proceed requires a second explicit confirmation with the exact phrase "PROCEED WITHOUT APPROVAL".');
|
|
138
|
+
}
|
|
139
|
+
if (stage !== "plan") {
|
|
140
|
+
this.store.saveReview(run, { stage, at: now(), approved: decision === "approve", fallback: "text", ...(reason ? { feedback: reason } : {}) });
|
|
141
|
+
}
|
|
142
|
+
if (decision === "stop") {
|
|
143
|
+
this.pause(run);
|
|
144
|
+
} else {
|
|
145
|
+
delete run.pendingTextReview;
|
|
146
|
+
if (stage === "plan") run.phase = transition(run.phase, decision === "revise" ? "planning" : "approved");
|
|
147
|
+
else if (decision === "revise") run.phase = transition(run.phase, "repairing");
|
|
148
|
+
else run.phase = transition(run.phase, stage === "final" ? "complete" : "final-review");
|
|
149
|
+
}
|
|
150
|
+
if (decision === "proceed") run.completionOverride = true;
|
|
151
|
+
this.store.appendDecision(run, { at: now(), kind: `${stage}-review`, actor: "user", decision, ...(reason ? { reason } : {}), ...(decision === "proceed" ? { warned: true } : {}) });
|
|
152
|
+
if (run.phase === "complete") this.finish(run);
|
|
153
|
+
return { run, message: `Recorded ${stage} decision: ${decision}. Current phase: ${run.phase}.` };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
beginImplementation(): WorkflowResult {
|
|
157
|
+
const run = this.requireActive();
|
|
158
|
+
if (run.phase !== "approved") throw new Error(`Implementation is blocked during ${run.phase}.`);
|
|
159
|
+
run.phase = transition(run.phase, "implementing");
|
|
160
|
+
this.store.appendDecision(run, { at: now(), kind: "implementation", actor: "agent", decision: "started" });
|
|
161
|
+
return { run, message: "Implementation gate opened. Preserve unrelated work and do not commit, stash, reset, or push." };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
recordQa(report: QaReport): WorkflowResult {
|
|
165
|
+
const run = this.requireActive();
|
|
166
|
+
if (run.phase === "implementing" || run.phase === "repairing") run.phase = transition(run.phase, "qa");
|
|
167
|
+
if (run.phase !== "qa") throw new Error(`Cannot record QA during ${run.phase}.`);
|
|
168
|
+
if (report.profile !== run.qaProfile) throw new Error(`QA profile mismatch: expected ${run.qaProfile}.`);
|
|
169
|
+
this.store.saveQa(run, report);
|
|
170
|
+
run.changedFiles = changedFiles(this.cwd, run.baselineFiles);
|
|
171
|
+
if (report.blockingFailures.length > 0) run.phase = transition(run.phase, "repairing");
|
|
172
|
+
this.store.save(run);
|
|
173
|
+
const hasInitialReview = run.reviews.some((review) => review.stage === "initial");
|
|
174
|
+
return { run, message: report.blockingFailures.length ? `QA found ${report.blockingFailures.length} blocking failure(s). Repair without opening a review tab.` : hasInitialReview ? "QA passed after initial review. Open the one final review tab." : "QA passed. Open the required initial code review." };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
recordRepair(summary: string): WorkflowResult {
|
|
178
|
+
const run = this.requireActive();
|
|
179
|
+
if (run.phase !== "repairing") throw new Error(`Cannot record a repair during ${run.phase}.`);
|
|
180
|
+
if (run.repairCount >= this.config.repairs.maxAttempts) throw new Error("The maximum of three repair attempts has been reached.");
|
|
181
|
+
run.repairCount += 1;
|
|
182
|
+
this.store.saveRepair(run, summary);
|
|
183
|
+
run.phase = transition(run.phase, "qa");
|
|
184
|
+
this.store.appendDecision(run, { at: now(), kind: "repair", actor: "agent", decision: `completed-${run.repairCount}`, reason: summary.slice(0, 500) });
|
|
185
|
+
return { run, message: `Repair ${run.repairCount}/${this.config.repairs.maxAttempts} recorded. Re-run QA; do not open an intermediate browser review.` };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async review(stage: Exclude<ReviewStage, "plan">, ctx: ExtensionContext, signal?: AbortSignal): Promise<WorkflowResult> {
|
|
189
|
+
const run = this.requireActive();
|
|
190
|
+
const hasInitialReview = run.reviews.some((review) => review.stage === "initial");
|
|
191
|
+
if (stage === "initial" && run.phase !== "qa") throw new Error(`Initial review requires QA phase, not ${run.phase}.`);
|
|
192
|
+
if (stage === "initial" && hasInitialReview) throw new Error("The initial review has already run; intermediate repair tabs are forbidden. Run the final review after QA.");
|
|
193
|
+
if (stage === "final" && !hasInitialReview) throw new Error("Final review is blocked until the required initial review has completed.");
|
|
194
|
+
if (stage === "final" && !["qa", "initial-review", "final-review"].includes(run.phase)) throw new Error(`Final review cannot start during ${run.phase}.`);
|
|
195
|
+
run.phase = stage === "initial" ? transition(run.phase, "initial-review") : (run.phase === "final-review" ? run.phase : transition(run.phase, "final-review"));
|
|
196
|
+
this.store.save(run);
|
|
197
|
+
const response = await this.client.codeReview(this.cwd, signal);
|
|
198
|
+
if (response.status !== "handled") {
|
|
199
|
+
run.phase = transition(run.phase, "awaiting-text-approval");
|
|
200
|
+
run.pendingTextReview = stage;
|
|
201
|
+
this.store.save(run);
|
|
202
|
+
return { run, message: `Plannotator ${stage} review unavailable${response.error ? `: ${response.error}` : ""}. Present the diff and QA evidence in chat, then record approve, revise, proceed, or stop.`, needsTextDecision: true };
|
|
203
|
+
}
|
|
204
|
+
const review: ReviewRecord = { stage, at: now(), approved: response.result.approved, ...(response.result.feedback ? { feedback: response.result.feedback } : {}), ...(response.result.annotations ? { annotations: response.result.annotations } : {}) };
|
|
205
|
+
this.store.saveReview(run, review);
|
|
206
|
+
if (review.approved) {
|
|
207
|
+
if (stage === "initial") {
|
|
208
|
+
run.phase = transition(run.phase, "final-review");
|
|
209
|
+
this.store.save(run);
|
|
210
|
+
return { run, message: "Initial review approved. Open the one final review tab now; no intermediate repair tab is needed." };
|
|
211
|
+
}
|
|
212
|
+
run.phase = transition(run.phase, "complete");
|
|
213
|
+
this.finish(run);
|
|
214
|
+
return { run, message: "Final review approved. Run complete." };
|
|
215
|
+
}
|
|
216
|
+
const choice = await chooseAfterRejection(ctx, run, review.feedback);
|
|
217
|
+
if (choice === "proceed") {
|
|
218
|
+
run.completionOverride = true;
|
|
219
|
+
run.phase = stage === "final" ? transition(run.phase, "complete") : transition(run.phase, "final-review");
|
|
220
|
+
this.store.appendDecision(run, { at: now(), kind: `${stage}-review`, actor: "user", decision: "proceed-anyway", reason: review.feedback ?? "Review rejected or closed.", warned: true });
|
|
221
|
+
if (run.phase === "complete") this.finish(run);
|
|
222
|
+
return { run, message: `Review override recorded. Current phase: ${run.phase}.` };
|
|
223
|
+
}
|
|
224
|
+
if (choice === "stop") {
|
|
225
|
+
run.phase = transition(run.phase, "awaiting-text-approval");
|
|
226
|
+
run.pendingTextReview = stage;
|
|
227
|
+
this.pause(run);
|
|
228
|
+
this.store.appendDecision(run, { at: now(), kind: "run", actor: "user", decision: `stopped-after-${stage}-review` });
|
|
229
|
+
return { run, message: "Run stopped." };
|
|
230
|
+
}
|
|
231
|
+
if (choice === "text-required") {
|
|
232
|
+
run.phase = transition(run.phase, "awaiting-text-approval");
|
|
233
|
+
run.pendingTextReview = stage;
|
|
234
|
+
this.store.save(run);
|
|
235
|
+
return { run, message: `Review not approved${review.feedback ? `: ${review.feedback}` : ""}. Ask whether to revise, proceed with warning, or stop.`, needsTextDecision: true };
|
|
236
|
+
}
|
|
237
|
+
if (run.repairCount >= this.config.repairs.maxAttempts) {
|
|
238
|
+
run.phase = transition(run.phase, "awaiting-text-approval");
|
|
239
|
+
run.pendingTextReview = stage;
|
|
240
|
+
this.store.save(run);
|
|
241
|
+
return { run, message: "Review remains unapproved after three repairs. Ask the user to proceed with warning or stop.", needsTextDecision: true };
|
|
242
|
+
}
|
|
243
|
+
run.phase = transition(run.phase, "repairing");
|
|
244
|
+
this.store.appendDecision(run, { at: now(), kind: `${stage}-review`, actor: "user", decision: "revise", ...(review.feedback ? { reason: review.feedback } : {}) });
|
|
245
|
+
return { run, message: `Repair review findings without opening a new browser tab. Feedback: ${review.feedback ?? "See annotations."}` };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
stop(reason?: string): WorkflowResult {
|
|
249
|
+
const run = this.requireActive();
|
|
250
|
+
if (!["complete", "stopped"].includes(run.phase)) this.pause(run);
|
|
251
|
+
this.store.appendDecision(run, { at: now(), kind: "run", actor: "user", decision: "stopped", ...(reason ? { reason } : {}) });
|
|
252
|
+
return { run, message: "Run stopped and preserved for resume." };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
resume(): WorkflowResult {
|
|
256
|
+
const run = this.requireActive();
|
|
257
|
+
if (run.phase === "stopped") {
|
|
258
|
+
const resumePhase = run.resumePhase as RunRecord["phase"] | undefined;
|
|
259
|
+
if (!resumePhase || !SAFE_RESUME_PHASES.has(resumePhase)) {
|
|
260
|
+
throw new Error("Stopped run has no safe non-mutating resume phase.");
|
|
261
|
+
}
|
|
262
|
+
run.phase = resumePhase;
|
|
263
|
+
delete run.resumePhase;
|
|
264
|
+
this.store.appendDecision(run, { at: now(), kind: "run", actor: "user", decision: "resumed", reason: `Resumed at safe phase ${run.phase}.` });
|
|
265
|
+
}
|
|
266
|
+
if (run.phase === "failed") throw new Error("Failed runs require an explicit new plan; they cannot resume directly into mutation.");
|
|
267
|
+
return { run, message: `Resume run ${run.id} from ${run.phase}. Read its audit record before acting.` };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private pause(run: RunRecord): void {
|
|
271
|
+
if (["complete", "stopped", "failed"].includes(run.phase)) return;
|
|
272
|
+
const resumePhase = run.phase === "implementing" ? "approved" : run.phase === "repairing" ? "qa" : run.phase;
|
|
273
|
+
if (resumePhase === "complete" || resumePhase === "stopped" || resumePhase === "failed") throw new Error("Cannot pause a terminal run.");
|
|
274
|
+
run.resumePhase = resumePhase;
|
|
275
|
+
run.phase = transition(run.phase, "stopped");
|
|
276
|
+
this.store.save(run);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private finish(run: RunRecord): void {
|
|
280
|
+
run.changedFiles = changedFiles(this.cwd, run.baselineFiles);
|
|
281
|
+
this.store.save(run);
|
|
282
|
+
this.store.saveFinalReport(run, finalReport(run));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private requireActive(): RunRecord {
|
|
286
|
+
const run = this.active();
|
|
287
|
+
if (!run) throw new Error("No Implannotator run exists in this project.");
|
|
288
|
+
return run;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RunPhase } from "../domain/types.js";
|
|
2
|
+
|
|
3
|
+
const ALLOWED: Record<RunPhase, readonly RunPhase[]> = {
|
|
4
|
+
inspecting: ["planning", "stopped", "failed"],
|
|
5
|
+
planning: ["awaiting-plan-review", "awaiting-text-approval", "approved", "stopped", "failed"],
|
|
6
|
+
"awaiting-plan-review": ["planning", "approved", "awaiting-text-approval", "stopped", "failed"],
|
|
7
|
+
"awaiting-text-approval": ["planning", "approved", "repairing", "final-review", "complete", "stopped", "failed"],
|
|
8
|
+
approved: ["implementing", "stopped", "failed"],
|
|
9
|
+
implementing: ["qa", "stopped", "failed"],
|
|
10
|
+
qa: ["initial-review", "repairing", "final-review", "stopped", "failed"],
|
|
11
|
+
"initial-review": ["awaiting-text-approval", "repairing", "final-review", "stopped", "failed"],
|
|
12
|
+
repairing: ["qa", "final-review", "stopped", "failed"],
|
|
13
|
+
"final-review": ["awaiting-text-approval", "complete", "repairing", "stopped", "failed"],
|
|
14
|
+
complete: [],
|
|
15
|
+
stopped: [],
|
|
16
|
+
failed: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function canTransition(from: RunPhase, to: RunPhase): boolean {
|
|
20
|
+
return ALLOWED[from].includes(to);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function transition(from: RunPhase, to: RunPhase): RunPhase {
|
|
24
|
+
if (!canTransition(from, to)) throw new Error(`Illegal Implannotator transition: ${from} -> ${to}`);
|
|
25
|
+
return to;
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "implannotator",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Approval-gated frontend implementation for Pi, combining Implannotator design guidance with Plannotator reviews.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "yoseph_23",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/yosefdot/implannotator.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/yosefdot/implannotator#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/yosefdot/implannotator/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"pi",
|
|
19
|
+
"extension",
|
|
20
|
+
"skill",
|
|
21
|
+
"frontend",
|
|
22
|
+
"design",
|
|
23
|
+
"approval",
|
|
24
|
+
"plannotator",
|
|
25
|
+
"code-review"
|
|
26
|
+
],
|
|
27
|
+
"files": [
|
|
28
|
+
"extensions",
|
|
29
|
+
"skills",
|
|
30
|
+
"schemas",
|
|
31
|
+
"scripts",
|
|
32
|
+
"docs",
|
|
33
|
+
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"THIRD_PARTY_NOTICES.md"
|
|
37
|
+
],
|
|
38
|
+
"pi": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"extensions/index.ts"
|
|
41
|
+
],
|
|
42
|
+
"skills": [
|
|
43
|
+
"skills/implannotator"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"implannotator": {
|
|
47
|
+
"plannotatorCompanion": "^0.23.1"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"lint": "oxlint extensions tests scripts",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"test:compat": "node scripts/check-plannotator-compat.mjs",
|
|
55
|
+
"verify:snapshot": "node scripts/verify-implannotator-snapshot.mjs",
|
|
56
|
+
"verify": "npm run verify:snapshot && npm run typecheck && npm run lint && npm test",
|
|
57
|
+
"update-check": "node scripts/update-check.mjs",
|
|
58
|
+
"pack:check": "npm pack --dry-run",
|
|
59
|
+
"prepublishOnly": "npm run verify && node scripts/assert-publish-ready.mjs"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@earendil-works/pi-ai": "*",
|
|
63
|
+
"@earendil-works/pi-coding-agent": ">=0.80.7"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@earendil-works/pi-ai": "^0.80.7",
|
|
67
|
+
"@plannotator/pi-extension": "^0.23.1",
|
|
68
|
+
"@earendil-works/pi-coding-agent": "^0.80.7",
|
|
69
|
+
"@types/node": "^24.0.0",
|
|
70
|
+
"oxlint": "^1.50.0",
|
|
71
|
+
"typescript": "^5.9.0",
|
|
72
|
+
"vitest": "^4.0.0"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=20"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/yosefdot/implannotator/schemas/config.schema.json",
|
|
4
|
+
"title": "Implannotator configuration",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"schemaVersion": { "const": 1 },
|
|
8
|
+
"approval": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"required": { "const": true },
|
|
12
|
+
"allowLowRiskAutoApprove": { "type": "boolean", "default": false },
|
|
13
|
+
"proceedRequiresSecondConfirmation": { "const": true },
|
|
14
|
+
"browserTimeoutMs": { "type": "integer", "minimum": 1000 }
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
},
|
|
18
|
+
"repairs": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"maxAttempts": { "type": "integer", "minimum": 0, "maximum": 3 },
|
|
22
|
+
"reopenOnlyForFinalReview": { "const": true }
|
|
23
|
+
},
|
|
24
|
+
"additionalProperties": false
|
|
25
|
+
},
|
|
26
|
+
"qa": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"defaultProfile": { "enum": ["adaptive", "quick", "standard", "full"] },
|
|
30
|
+
"requireEvidence": { "type": "boolean" }
|
|
31
|
+
},
|
|
32
|
+
"additionalProperties": false
|
|
33
|
+
},
|
|
34
|
+
"audit": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"directory": { "type": "string", "default": ".implannotator/runs" },
|
|
38
|
+
"retentionDays": { "type": "integer", "minimum": 1 },
|
|
39
|
+
"redactSensitiveValues": { "const": true }
|
|
40
|
+
},
|
|
41
|
+
"additionalProperties": false
|
|
42
|
+
},
|
|
43
|
+
"git": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"allowAutomaticCommit": { "const": false },
|
|
47
|
+
"allowAutomaticStash": { "const": false },
|
|
48
|
+
"preserveExistingChanges": { "const": true }
|
|
49
|
+
},
|
|
50
|
+
"additionalProperties": false
|
|
51
|
+
},
|
|
52
|
+
"subagents": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"enabled": { "type": "boolean" },
|
|
56
|
+
"readOnly": { "const": true },
|
|
57
|
+
"mainAgentSoleWriter": { "const": true }
|
|
58
|
+
},
|
|
59
|
+
"additionalProperties": false
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"additionalProperties": false
|
|
63
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/yosefdot/implannotator/schemas/decision.schema.json",
|
|
4
|
+
"title": "Implannotator decision",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["at", "kind", "actor", "decision"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"at": { "type": "string", "format": "date-time" },
|
|
9
|
+
"kind": { "type": "string" },
|
|
10
|
+
"actor": { "enum": ["user", "agent", "system"] },
|
|
11
|
+
"decision": { "type": "string" },
|
|
12
|
+
"reason": { "type": "string" },
|
|
13
|
+
"warned": { "type": "boolean" }
|
|
14
|
+
},
|
|
15
|
+
"additionalProperties": false
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/yosefdot/implannotator/schemas/qa-report.schema.json",
|
|
4
|
+
"title": "Implannotator QA report",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "profile", "createdAt", "checks", "blockingFailures"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": { "const": 1 },
|
|
9
|
+
"profile": { "enum": ["quick", "standard", "full"] },
|
|
10
|
+
"checks": { "type": "array" },
|
|
11
|
+
"blockingFailures": { "type": "array", "items": { "type": "string" } }
|
|
12
|
+
},
|
|
13
|
+
"additionalProperties": false
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/yosefdot/implannotator/schemas/run.schema.json",
|
|
4
|
+
"title": "Implannotator run",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "id", "cwd", "command", "risk", "phase", "createdAt", "updatedAt", "repairCount", "qaProfile", "changedFiles", "decisions", "reviews"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": { "const": 1 },
|
|
9
|
+
"id": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" },
|
|
10
|
+
"cwd": { "type": "string" },
|
|
11
|
+
"phase": { "enum": ["inspecting", "planning", "awaiting-plan-review", "awaiting-text-approval", "approved", "implementing", "qa", "initial-review", "repairing", "final-review", "complete", "stopped", "failed"] },
|
|
12
|
+
"repairCount": { "type": "integer", "minimum": 0, "maximum": 3 }
|
|
13
|
+
},
|
|
14
|
+
"additionalProperties": true
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const expected = "I_CONFIRMED_IMPECCABLE_REDISTRIBUTION_RIGHTS";
|
|
2
|
+
if (process.env.IMPLANNOTATOR_PUBLISH_ACK !== expected) {
|
|
3
|
+
console.error("Publishing blocked: confirm the redistribution license/attribution for the adapted Impeccable snapshot, then set IMPLANNOTATOR_PUBLISH_ACK=I_CONFIRMED_IMPECCABLE_REDISTRIBUTION_RIGHTS for the release command.");
|
|
4
|
+
process.exit(1);
|
|
5
|
+
}
|
|
6
|
+
console.log("Explicit redistribution acknowledgement received.");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const packagePath = require.resolve("@plannotator/pi-extension/package.json");
|
|
7
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
8
|
+
const eventsPath = resolve(dirname(packagePath), "plannotator-events.ts");
|
|
9
|
+
const events = readFileSync(eventsPath, "utf8");
|
|
10
|
+
const required = [
|
|
11
|
+
'"plannotator:request"',
|
|
12
|
+
'"plannotator:review-result"',
|
|
13
|
+
'| "plan-review"',
|
|
14
|
+
'| "review-status"',
|
|
15
|
+
'| "code-review"',
|
|
16
|
+
"respond: (response:",
|
|
17
|
+
];
|
|
18
|
+
const missing = required.filter((token) => !events.includes(token));
|
|
19
|
+
if (missing.length) {
|
|
20
|
+
console.error(`Plannotator ${pkg.version} is incompatible; missing public contract tokens: ${missing.join(", ")}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
console.log(`Plannotator ${pkg.version} public event contract is compatible.`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
|
|
10
|
+
const required = manifest.implannotator?.plannotatorCompanion ?? "unknown";
|
|
11
|
+
let developmentVersion;
|
|
12
|
+
try {
|
|
13
|
+
developmentVersion = require("@plannotator/pi-extension/package.json").version;
|
|
14
|
+
} catch {
|
|
15
|
+
developmentVersion = undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const published = JSON.parse(execFileSync("npm", ["view", "@plannotator/pi-extension", "version", "--json"], { encoding: "utf8" }));
|
|
20
|
+
const latest = Array.isArray(published) ? published.at(-1) : published;
|
|
21
|
+
if (typeof latest !== "string") throw new Error("npm returned no published version.");
|
|
22
|
+
console.log(JSON.stringify({
|
|
23
|
+
companion: "@plannotator/pi-extension",
|
|
24
|
+
required,
|
|
25
|
+
developmentVersion: developmentVersion ?? null,
|
|
26
|
+
latest,
|
|
27
|
+
install: "pi install npm:@plannotator/pi-extension",
|
|
28
|
+
}, null, 2));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(`Could not check npm: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const skill = resolve(root, "skills", "implannotator");
|
|
8
|
+
const manifest = JSON.parse(readFileSync(resolve(skill, "vendor", "impeccable-manifest.json"), "utf8"));
|
|
9
|
+
const failures = [];
|
|
10
|
+
for (const entry of manifest.files) {
|
|
11
|
+
const path = resolve(skill, entry.adaptedPath);
|
|
12
|
+
if (!existsSync(path)) { failures.push(`missing ${entry.adaptedPath}`); continue; }
|
|
13
|
+
const hash = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
14
|
+
if (hash !== entry.adaptedSha256) failures.push(`changed ${entry.adaptedPath}`);
|
|
15
|
+
}
|
|
16
|
+
const skillText = readFileSync(resolve(skill, "SKILL.md"), "utf8");
|
|
17
|
+
for (const required of ["name: implannotator", "implannotator_control", "reference/implannotator-workflow.md", "stage: \"final\""]) {
|
|
18
|
+
if (!skillText.includes(required)) failures.push(`SKILL.md missing ${required}`);
|
|
19
|
+
}
|
|
20
|
+
if (failures.length) {
|
|
21
|
+
console.error(`Implannotator snapshot verification failed:\n- ${failures.join("\n- ")}\nRun the documented snapshot-update procedure only for intentional changes.`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
console.log(`Verified ${manifest.files.length} adapted source files and the mandatory workflow contract.`);
|