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,40 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { RunRecord } from "../domain/types.js";
|
|
4
|
+
|
|
5
|
+
const TERMINAL = new Set(["complete", "stopped", "failed"]);
|
|
6
|
+
const PRE_APPROVAL = new Set(["inspecting", "planning", "awaiting-plan-review", "awaiting-text-approval"]);
|
|
7
|
+
const READ_ONLY_BASH = /^\s*(pwd|ls|find|rg|grep|head|tail|wc|stat|file|git\s+(status|diff|log|show|branch)|npm\s+(view|list|ls)|node\s+--version)\b/;
|
|
8
|
+
const FORBIDDEN_GIT = /\bgit\s+(commit|stash|reset|clean|push|checkout|switch|restore|rebase|merge)\b/i;
|
|
9
|
+
|
|
10
|
+
function pathInside(cwd: string, path: string): boolean {
|
|
11
|
+
const rel = relative(resolve(cwd), resolve(cwd, path));
|
|
12
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function registerMutationGate(pi: ExtensionAPI, activeRun: (cwd: string) => RunRecord | undefined): void {
|
|
16
|
+
pi.on("tool_call", (event, ctx) => {
|
|
17
|
+
const run = activeRun(ctx.cwd);
|
|
18
|
+
if (!run || TERMINAL.has(run.phase) || event.toolName === "implannotator_control") return;
|
|
19
|
+
|
|
20
|
+
if (event.toolName === "bash") {
|
|
21
|
+
const command = typeof event.input.command === "string" ? event.input.command : "";
|
|
22
|
+
if (FORBIDDEN_GIT.test(command)) {
|
|
23
|
+
return { block: true, reason: "Implannotator never performs automatic stash/reset/commit/push or other destructive Git operations." };
|
|
24
|
+
}
|
|
25
|
+
if (PRE_APPROVAL.has(run.phase) && !READ_ONLY_BASH.test(command)) {
|
|
26
|
+
return { block: true, reason: `Implannotator approval gate: mutating shell commands are blocked during ${run.phase}.` };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if ((event.toolName === "write" || event.toolName === "edit") && PRE_APPROVAL.has(run.phase)) {
|
|
31
|
+
return { block: true, reason: `Implannotator approval gate: file mutation is blocked during ${run.phase}. Submit and approve the plan first.` };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (event.toolName === "write" || event.toolName === "edit") {
|
|
35
|
+
const input = event.input as Record<string, unknown>;
|
|
36
|
+
const path = typeof input.path === "string" ? input.path : typeof input.file_path === "string" ? input.file_path : undefined;
|
|
37
|
+
if (path && !pathInside(ctx.cwd, path)) return { block: true, reason: "Implannotator blocks writes outside the active project." };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const EXCLUDED = new Set([".git", ".implannotator", "node_modules", "dist", "build", "coverage", ".next"]);
|
|
6
|
+
const MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
export function snapshotFiles(cwd: string): Record<string, string> {
|
|
9
|
+
const result: Record<string, string> = {};
|
|
10
|
+
const visit = (directory: string) => {
|
|
11
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
12
|
+
if (EXCLUDED.has(entry.name)) continue;
|
|
13
|
+
const path = resolve(directory, entry.name);
|
|
14
|
+
if (entry.isDirectory()) { visit(path); continue; }
|
|
15
|
+
if (!entry.isFile()) continue;
|
|
16
|
+
const stat = statSync(path);
|
|
17
|
+
if (stat.size > MAX_FILE_BYTES) continue;
|
|
18
|
+
result[relative(cwd, path).replaceAll("\\", "/")] = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
visit(cwd);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function diffSnapshots(before: Record<string, string>, after: Record<string, string>): string[] {
|
|
26
|
+
return [...new Set([...Object.keys(before), ...Object.keys(after)])]
|
|
27
|
+
.filter((path) => before[path] !== after[path])
|
|
28
|
+
.sort();
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { QaProfile } from "../domain/types.js";
|
|
2
|
+
|
|
3
|
+
const QUICK = [
|
|
4
|
+
"build-tests-types-lint",
|
|
5
|
+
"targeted-viewport",
|
|
6
|
+
"console-page-network-errors",
|
|
7
|
+
"keyboard-focus",
|
|
8
|
+
"accessibility-contrast",
|
|
9
|
+
"screenshot",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const STANDARD = [
|
|
13
|
+
...QUICK,
|
|
14
|
+
"desktop-tablet-mobile",
|
|
15
|
+
"touch-targets-overflow",
|
|
16
|
+
"loading-empty-error-long-content",
|
|
17
|
+
"themes-reduced-motion",
|
|
18
|
+
"content-resilience",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const FULL = [
|
|
22
|
+
...STANDARD,
|
|
23
|
+
"reference-visual-comparison",
|
|
24
|
+
"performance-core-web-vitals",
|
|
25
|
+
"cross-browser-when-available",
|
|
26
|
+
"internationalization-resilience",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function requiredChecks(profile: QaProfile): string[] {
|
|
30
|
+
return [...(profile === "quick" ? QUICK : profile === "standard" ? STANDARD : FULL)];
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { QaProfile, RiskLevel } from "../domain/types.js";
|
|
2
|
+
|
|
3
|
+
export interface QaScope {
|
|
4
|
+
changedFileCount: number;
|
|
5
|
+
sharedComponents?: boolean;
|
|
6
|
+
navigation?: boolean;
|
|
7
|
+
newPage?: boolean;
|
|
8
|
+
redesign?: boolean;
|
|
9
|
+
authOrPayments?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function classifyRisk(scope: QaScope): RiskLevel {
|
|
13
|
+
if (scope.authOrPayments || scope.navigation || scope.redesign) return "high";
|
|
14
|
+
if (scope.newPage || scope.sharedComponents || scope.changedFileCount > 3) return "medium";
|
|
15
|
+
return "low";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function selectQaProfile(scope: QaScope): QaProfile {
|
|
19
|
+
const risk = classifyRisk(scope);
|
|
20
|
+
if (risk === "high" || scope.newPage || scope.redesign) return "full";
|
|
21
|
+
if (risk === "medium" || scope.changedFileCount > 1) return "standard";
|
|
22
|
+
return "quick";
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RunRecord } from "../domain/types.js";
|
|
2
|
+
|
|
3
|
+
export function finalReport(run: RunRecord): string {
|
|
4
|
+
const finalReview = [...run.reviews].reverse().find((review) => review.stage === "final");
|
|
5
|
+
return `# Implannotator run ${run.id}\n\n- Status: ${run.phase}\n- Command: ${run.command}${run.target ? ` ${run.target}` : ""}\n- QA profile: ${run.qaProfile}\n- Repairs: ${run.repairCount}/3\n- Changed files: ${run.changedFiles.length}\n- Final review: ${finalReview?.approved ? "approved" : run.completionOverride ? "overridden with warning" : "not approved"}\n- Created: ${run.createdAt}\n- Updated: ${run.updatedAt}\n`;
|
|
6
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
export function atomicWrite(path: string, content: string): void {
|
|
6
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
7
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
8
|
+
writeFileSync(temp, content, { encoding: "utf8", mode: 0o600 });
|
|
9
|
+
renameSync(temp, path);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function atomicWriteJson(path: string, value: unknown): void {
|
|
13
|
+
atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const SECRET_PATTERNS: Array<[RegExp, string]> = [
|
|
2
|
+
[/(api[_-]?key|access[_-]?token|auth[_-]?token|password|secret)(\s*[:=]\s*)(["']?)[^\s,"']{8,}\3/gi, "$1$2[REDACTED]"],
|
|
3
|
+
[/\bsk-[a-zA-Z0-9_-]{16,}\b/g, "[REDACTED]"],
|
|
4
|
+
[/\bgh[pousr]_[a-zA-Z0-9]{20,}\b/g, "[REDACTED]"],
|
|
5
|
+
[/\b(Bearer\s+)[a-zA-Z0-9._~+/-]{16,}/gi, "$1[REDACTED]"],
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export function redactText(value: string): string {
|
|
9
|
+
return SECRET_PATTERNS.reduce((result, [pattern, replacement]) => result.replace(pattern, replacement), value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function redactValue(value: unknown): unknown {
|
|
13
|
+
if (typeof value === "string") return redactText(value);
|
|
14
|
+
if (Array.isArray(value)) return value.map(redactValue);
|
|
15
|
+
if (value && typeof value === "object") {
|
|
16
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, /password|secret|token|api.?key/i.test(key) ? "[REDACTED]" : redactValue(item)]));
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface RunLayout {
|
|
4
|
+
root: string;
|
|
5
|
+
runJson: string;
|
|
6
|
+
plan: string;
|
|
7
|
+
decisions: string;
|
|
8
|
+
changedFiles: string;
|
|
9
|
+
qaReport: string;
|
|
10
|
+
reviews: string;
|
|
11
|
+
repairs: string;
|
|
12
|
+
evidence: string;
|
|
13
|
+
finalReport: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function assertInside(parent: string, child: string): string {
|
|
17
|
+
const resolvedParent = resolve(parent);
|
|
18
|
+
const resolvedChild = resolve(child);
|
|
19
|
+
const rel = relative(resolvedParent, resolvedChild);
|
|
20
|
+
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error(`Path escapes audit root: ${child}`);
|
|
21
|
+
return resolvedChild;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function runLayout(cwd: string, auditDirectory: string, id: string): RunLayout {
|
|
25
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) throw new Error("Invalid run id.");
|
|
26
|
+
const auditRoot = assertInside(cwd, resolve(cwd, auditDirectory));
|
|
27
|
+
const root = assertInside(auditRoot, resolve(auditRoot, id));
|
|
28
|
+
return {
|
|
29
|
+
root,
|
|
30
|
+
runJson: resolve(root, "run.json"),
|
|
31
|
+
plan: resolve(root, "plan.md"),
|
|
32
|
+
decisions: resolve(root, "decisions.ndjson"),
|
|
33
|
+
changedFiles: resolve(root, "changed-files.json"),
|
|
34
|
+
qaReport: resolve(root, "qa", "report.json"),
|
|
35
|
+
reviews: resolve(root, "reviews"),
|
|
36
|
+
repairs: resolve(root, "repairs"),
|
|
37
|
+
evidence: resolve(root, "evidence"),
|
|
38
|
+
finalReport: resolve(root, "final-report.md"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import type { DecisionRecord, QaReport, ReviewRecord, RunRecord } from "../domain/types.js";
|
|
5
|
+
import { atomicWrite, atomicWriteJson } from "./atomic-file.js";
|
|
6
|
+
import { redactText, redactValue } from "./redaction.js";
|
|
7
|
+
import { runLayout } from "./run-layout.js";
|
|
8
|
+
|
|
9
|
+
export class RunStore {
|
|
10
|
+
readonly auditRoot: string;
|
|
11
|
+
readonly activePointer: string;
|
|
12
|
+
|
|
13
|
+
constructor(readonly cwd: string, readonly auditDirectory: string) {
|
|
14
|
+
this.auditRoot = resolve(cwd, auditDirectory);
|
|
15
|
+
this.activePointer = join(dirname(this.auditRoot), "active-run");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
create(command: string, target: string | undefined, risk: RunRecord["risk"], qaProfile: RunRecord["qaProfile"]): RunRecord {
|
|
19
|
+
const now = new Date().toISOString();
|
|
20
|
+
const id = `${now.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
21
|
+
const run: RunRecord = {
|
|
22
|
+
schemaVersion: 1,
|
|
23
|
+
id,
|
|
24
|
+
cwd: this.cwd,
|
|
25
|
+
command,
|
|
26
|
+
...(target ? { target } : {}),
|
|
27
|
+
risk,
|
|
28
|
+
phase: "inspecting",
|
|
29
|
+
createdAt: now,
|
|
30
|
+
updatedAt: now,
|
|
31
|
+
repairCount: 0,
|
|
32
|
+
qaProfile,
|
|
33
|
+
changedFiles: [],
|
|
34
|
+
decisions: [],
|
|
35
|
+
reviews: [],
|
|
36
|
+
};
|
|
37
|
+
this.save(run);
|
|
38
|
+
atomicWrite(this.activePointer, `${id}\n`);
|
|
39
|
+
return run;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
save(run: RunRecord): void {
|
|
43
|
+
run.updatedAt = new Date().toISOString();
|
|
44
|
+
const layout = runLayout(this.cwd, this.auditDirectory, run.id);
|
|
45
|
+
mkdirSync(layout.root, { recursive: true, mode: 0o700 });
|
|
46
|
+
atomicWriteJson(layout.runJson, redactValue(run));
|
|
47
|
+
atomicWriteJson(layout.changedFiles, run.changedFiles);
|
|
48
|
+
if (run.plan !== undefined) {
|
|
49
|
+
const plan = redactText(run.plan);
|
|
50
|
+
atomicWrite(layout.plan, plan.endsWith("\n") ? plan : `${plan}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
load(id: string): RunRecord {
|
|
55
|
+
const path = runLayout(this.cwd, this.auditDirectory, id).runJson;
|
|
56
|
+
const run = JSON.parse(readFileSync(path, "utf8")) as RunRecord;
|
|
57
|
+
if (run.schemaVersion !== 1 || run.cwd !== this.cwd) throw new Error("Unsupported or misplaced run record.");
|
|
58
|
+
return run;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
active(): RunRecord | undefined {
|
|
62
|
+
if (!existsSync(this.activePointer)) return undefined;
|
|
63
|
+
const id = readFileSync(this.activePointer, "utf8").trim();
|
|
64
|
+
if (!id) return undefined;
|
|
65
|
+
try { return this.load(basename(id)); } catch { return undefined; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
appendDecision(run: RunRecord, decision: DecisionRecord): void {
|
|
69
|
+
run.decisions.push(decision);
|
|
70
|
+
const layout = runLayout(this.cwd, this.auditDirectory, run.id);
|
|
71
|
+
mkdirSync(dirname(layout.decisions), { recursive: true, mode: 0o700 });
|
|
72
|
+
appendFileSync(layout.decisions, `${JSON.stringify(redactValue(decision))}\n`, { encoding: "utf8", mode: 0o600 });
|
|
73
|
+
this.save(run);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
saveQa(run: RunRecord, report: QaReport): void {
|
|
77
|
+
atomicWriteJson(runLayout(this.cwd, this.auditDirectory, run.id).qaReport, redactValue(report));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
saveReview(run: RunRecord, review: ReviewRecord): void {
|
|
81
|
+
run.reviews.push(review);
|
|
82
|
+
const path = join(runLayout(this.cwd, this.auditDirectory, run.id).reviews, `${review.stage}-${run.reviews.length}.json`);
|
|
83
|
+
atomicWriteJson(path, redactValue(review));
|
|
84
|
+
this.save(run);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
saveRepair(run: RunRecord, summary: string): void {
|
|
88
|
+
const path = join(runLayout(this.cwd, this.auditDirectory, run.id).repairs, `${run.repairCount}.md`);
|
|
89
|
+
const redacted = redactText(summary);
|
|
90
|
+
atomicWrite(path, redacted.endsWith("\n") ? redacted : `${redacted}\n`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
saveFinalReport(run: RunRecord, report: string): void {
|
|
94
|
+
const redacted = redactText(report);
|
|
95
|
+
atomicWrite(runLayout(this.cwd, this.auditDirectory, run.id).finalReport, redacted.endsWith("\n") ? redacted : `${redacted}\n`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { QaReport, QaProfile, ReviewStage, RiskLevel } from "../domain/types.js";
|
|
4
|
+
import type { Orchestrator } from "../workflow/orchestrator.js";
|
|
5
|
+
|
|
6
|
+
const Parameters = Type.Object({
|
|
7
|
+
action: Type.Union([
|
|
8
|
+
Type.Literal("start"), Type.Literal("status"), Type.Literal("submit_plan"), Type.Literal("text_decision"),
|
|
9
|
+
Type.Literal("begin_implementation"), Type.Literal("record_qa"), Type.Literal("record_repair"),
|
|
10
|
+
Type.Literal("review"), Type.Literal("resume"), Type.Literal("stop"),
|
|
11
|
+
]),
|
|
12
|
+
command: Type.Optional(Type.String()),
|
|
13
|
+
target: Type.Optional(Type.String()),
|
|
14
|
+
risk: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")])),
|
|
15
|
+
qaProfile: Type.Optional(Type.Union([Type.Literal("quick"), Type.Literal("standard"), Type.Literal("full")])),
|
|
16
|
+
plan: Type.Optional(Type.String()),
|
|
17
|
+
reportJson: Type.Optional(Type.String()),
|
|
18
|
+
summary: Type.Optional(Type.String()),
|
|
19
|
+
stage: Type.Optional(Type.Union([Type.Literal("plan"), Type.Literal("initial"), Type.Literal("final")])),
|
|
20
|
+
decision: Type.Optional(Type.Union([Type.Literal("approve"), Type.Literal("revise"), Type.Literal("proceed"), Type.Literal("stop")])),
|
|
21
|
+
reason: Type.Optional(Type.String()),
|
|
22
|
+
warned: Type.Optional(Type.Boolean()),
|
|
23
|
+
confirmation: Type.Optional(Type.String()),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export function registerControlTool(pi: ExtensionAPI, getOrchestrator: (cwd: string) => Orchestrator): void {
|
|
27
|
+
pi.registerTool({
|
|
28
|
+
name: "implannotator_control",
|
|
29
|
+
label: "Implannotator",
|
|
30
|
+
description: "Control the approval-gated Implannotator frontend workflow. Start a run, submit a plan, open reviews, record QA/repairs, resume, or stop.",
|
|
31
|
+
promptSnippet: "Use implannotator_control at every Implannotator phase boundary.",
|
|
32
|
+
promptGuidelines: [
|
|
33
|
+
"Never begin file mutation until submit_plan returns an approved phase and begin_implementation succeeds.",
|
|
34
|
+
"Record complete QA evidence before review; never open browser review during intermediate repairs.",
|
|
35
|
+
],
|
|
36
|
+
parameters: Parameters,
|
|
37
|
+
executionMode: "sequential",
|
|
38
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
39
|
+
try {
|
|
40
|
+
const workflow = getOrchestrator(ctx.cwd);
|
|
41
|
+
let result;
|
|
42
|
+
switch (params.action) {
|
|
43
|
+
case "start":
|
|
44
|
+
result = workflow.start(params.command ?? "craft", params.target, (params.risk ?? "medium") as RiskLevel, (params.qaProfile ?? "standard") as QaProfile);
|
|
45
|
+
break;
|
|
46
|
+
case "status": {
|
|
47
|
+
const run = workflow.active();
|
|
48
|
+
if (!run) return output("No Implannotator run exists in this project.", undefined);
|
|
49
|
+
result = { run, message: `Run ${run.id}: ${run.phase}; repairs ${run.repairCount}/3; QA ${run.qaProfile}.` };
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case "submit_plan":
|
|
53
|
+
if (!params.plan?.trim()) throw new Error("submit_plan requires plan.");
|
|
54
|
+
result = await workflow.submitPlan(params.plan, ctx, signal);
|
|
55
|
+
break;
|
|
56
|
+
case "text_decision":
|
|
57
|
+
if (!params.stage || !params.decision) throw new Error("text_decision requires stage and decision.");
|
|
58
|
+
result = workflow.textDecision(params.stage as ReviewStage, params.decision, params.reason, params.warned ?? false, params.confirmation);
|
|
59
|
+
break;
|
|
60
|
+
case "begin_implementation":
|
|
61
|
+
result = workflow.beginImplementation();
|
|
62
|
+
break;
|
|
63
|
+
case "record_qa": {
|
|
64
|
+
if (!params.reportJson) throw new Error("record_qa requires reportJson.");
|
|
65
|
+
let report: QaReport;
|
|
66
|
+
try { report = JSON.parse(params.reportJson) as QaReport; } catch { throw new Error("reportJson must be valid JSON."); }
|
|
67
|
+
result = workflow.recordQa(report);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "record_repair":
|
|
71
|
+
if (!params.summary?.trim()) throw new Error("record_repair requires summary.");
|
|
72
|
+
result = workflow.recordRepair(params.summary);
|
|
73
|
+
break;
|
|
74
|
+
case "review":
|
|
75
|
+
if (params.stage !== "initial" && params.stage !== "final") throw new Error("review stage must be initial or final.");
|
|
76
|
+
result = await workflow.review(params.stage, ctx, signal);
|
|
77
|
+
break;
|
|
78
|
+
case "resume":
|
|
79
|
+
result = workflow.resume();
|
|
80
|
+
break;
|
|
81
|
+
case "stop":
|
|
82
|
+
result = workflow.stop(params.reason);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
return output(result.message, result.run);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], details: { ok: false }, isError: true };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function output(message: string, run: unknown) {
|
|
94
|
+
return { content: [{ type: "text" as const, text: message }], details: { ok: true, run } };
|
|
95
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { RunRecord } from "../domain/types.js";
|
|
3
|
+
|
|
4
|
+
export type RejectionChoice = "revise" | "proceed" | "stop";
|
|
5
|
+
|
|
6
|
+
export async function chooseAfterRejection(ctx: ExtensionContext, run: RunRecord, feedback?: string): Promise<RejectionChoice | "text-required"> {
|
|
7
|
+
if (!ctx.hasUI) return "text-required";
|
|
8
|
+
const choice = await ctx.ui.select(
|
|
9
|
+
"Implannotator review was not approved",
|
|
10
|
+
["Revise the plan or implementation", "Proceed anyway", "Stop"],
|
|
11
|
+
);
|
|
12
|
+
if (choice === "Proceed anyway") {
|
|
13
|
+
const confirmed = await ctx.ui.confirm(
|
|
14
|
+
"Proceed without approval?",
|
|
15
|
+
`This overrides the mandatory review gate and will be recorded in ${run.id}.${feedback ? `\n\nReview feedback: ${feedback}` : ""}`,
|
|
16
|
+
);
|
|
17
|
+
return confirmed ? "proceed" : "revise";
|
|
18
|
+
}
|
|
19
|
+
if (choice === "Stop" || !choice) return "stop";
|
|
20
|
+
return "revise";
|
|
21
|
+
}
|