@akagilnc/pi-workflow-roles 0.1.1751
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/LICENSE +202 -0
- package/README.md +104 -0
- package/README.zh-CN.md +133 -0
- package/THIRD_PARTY_NOTICES.md +60 -0
- package/dist/activation-ledger-git.js +68 -0
- package/dist/activation-ledger-session.js +120 -0
- package/dist/activation-ledger-topology.js +239 -0
- package/dist/activation-reconciliation.js +61 -0
- package/dist/audit-escalation.js +108 -0
- package/dist/auditor-dossier-tool.js +35 -0
- package/dist/canonical-json.js +78 -0
- package/dist/compliance-transport.js +77 -0
- package/dist/doctor-contracts.js +172 -0
- package/dist/dossier-resolution.js +103 -0
- package/dist/evidence-child-executor.js +661 -0
- package/dist/exact-utf8.js +12 -0
- package/dist/git-object-id.js +7 -0
- package/dist/in-process-session.js +50 -0
- package/dist/merger-contracts.js +76 -0
- package/dist/navigator-attendance.js +995 -0
- package/dist/navigator-invocation-identity.js +220 -0
- package/dist/open-tool-schema.js +39 -0
- package/dist/package-contracts/collector-output.js +50 -0
- package/dist/package-contracts/fixer-output.js +72 -0
- package/dist/package-contracts/fixer-packet.js +77 -0
- package/dist/package-contracts/judge-output.js +17 -0
- package/dist/package-contracts/reviewer-output.js +82 -0
- package/dist/package-contracts/terminating-tools.js +173 -0
- package/dist/package-contracts/worker-output.js +13 -0
- package/dist/package-owned-tool-idle.js +104 -0
- package/dist/packaged-role-registry.js +34 -0
- package/dist/public-cli/main.js +23867 -0
- package/dist/public-command-renderer.js +20 -0
- package/dist/reviewer-agent.js +93 -0
- package/dist/reviewer-child-executor.js +23 -0
- package/dist/reviewer-construction.js +95 -0
- package/dist/reviewer-dispatch.js +77 -0
- package/dist/reviewer-execution-ledger.js +160 -0
- package/dist/reviewer-failure-diagnostic.js +17 -0
- package/dist/reviewer-git-snapshot.js +38 -0
- package/dist/reviewer-pinned-git.js +146 -0
- package/dist/reviewer-preflight-error.js +15 -0
- package/dist/reviewer-prompt-identity.js +10 -0
- package/dist/reviewer-scope-prompt.js +21 -0
- package/dist/reviewer-workspace.js +151 -0
- package/dist/sha256.js +5 -0
- package/dist/sitian-record-entry.js +33 -0
- package/dist/stderr-jsonl.js +26 -0
- package/dist/stream-idle-guard.js +75 -0
- package/dist/tool-execution-observation.js +141 -0
- package/dist/uuidv7.js +21 -0
- package/dist/work-subject-identity.js +53 -0
- package/extensions/role-runtime.ts +303 -0
- package/package.json +69 -0
- package/packets/fixer-prerequisites.json +6 -0
- package/packets/fixer-repair.md +5 -0
- package/packets/judge-apply.md +77 -0
- package/packets/judge-authority.md +64 -0
- package/packets/judge-plan.md +55 -0
- package/packets/judge-review.md +49 -0
- package/packets/judge-submission.md +34 -0
- package/resources/methods/code-review/SKILL.md +92 -0
- package/resources/methods/code-review/agents/openai.yaml +3 -0
- package/resources/methods/code-review/provenance.json +26 -0
- package/resources/methods/diagnosing-bugs/SKILL.md +134 -0
- package/resources/methods/diagnosing-bugs/agents/openai.yaml +3 -0
- package/resources/methods/diagnosing-bugs/provenance.json +31 -0
- package/resources/methods/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/resources/methods/resolving-merge-conflicts/SKILL.md +14 -0
- package/resources/methods/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/resources/methods/resolving-merge-conflicts/provenance.json +26 -0
- package/resources/methods/tdd/SKILL.md +38 -0
- package/resources/methods/tdd/agents/openai.yaml +3 -0
- package/resources/methods/tdd/mocking.md +59 -0
- package/resources/methods/tdd/provenance.json +36 -0
- package/resources/methods/tdd/tests.md +77 -0
- package/resources/navigator-route-playbook.md +32 -0
- package/schemas/tool-execution-observation.schema.json +107 -0
- package/scripts/build-package.mjs +65 -0
- package/scripts/generate-tool-execution-observation-schema.ts +7 -0
- package/souls/coder.md +10 -0
- package/souls/collector.md +11 -0
- package/souls/doctor-auditor.md +23 -0
- package/souls/doctor.md +8 -0
- package/souls/fixer-auditor.md +33 -0
- package/souls/fixer.md +13 -0
- package/souls/judge-auditor.md +33 -0
- package/souls/judge.md +74 -0
- package/souls/merger.md +5 -0
- package/souls/navigator.md +5 -0
- package/souls/reviewer-auditor.md +25 -0
- package/souls/reviewer.md +11 -0
- package/src/activation-ledger-git.ts +96 -0
- package/src/activation-ledger-session.ts +188 -0
- package/src/activation-ledger-topology.ts +301 -0
- package/src/activation-ledger.ts +240 -0
- package/src/activation-reconciliation.ts +163 -0
- package/src/activation-trace.ts +38 -0
- package/src/audit-escalation.ts +177 -0
- package/src/auditor-dossier-tool.ts +48 -0
- package/src/auditor-soul.ts +28 -0
- package/src/canonical-json.ts +74 -0
- package/src/canonical-skill-binding.ts +107 -0
- package/src/collector-config.ts +89 -0
- package/src/collector-evidence.ts +461 -0
- package/src/collector-github.ts +656 -0
- package/src/collector-identity.ts +161 -0
- package/src/collector-ledger.ts +827 -0
- package/src/collector-receipt.ts +87 -0
- package/src/collector-role.ts +592 -0
- package/src/collector-tool-schemas.ts +19 -0
- package/src/compliance-transport.ts +130 -0
- package/src/doctor-auditor.ts +53 -0
- package/src/doctor-contracts.ts +166 -0
- package/src/doctor-evidence.ts +47 -0
- package/src/doctor-role.ts +18 -0
- package/src/dossier-resolution.ts +137 -0
- package/src/evidence-child-executor.ts +775 -0
- package/src/exact-utf8.ts +9 -0
- package/src/factory-board.ts +1822 -0
- package/src/git-object-id.ts +11 -0
- package/src/human-format.ts +65 -0
- package/src/in-process-session.ts +78 -0
- package/src/judge-auditor.ts +55 -0
- package/src/judge-recording-anti-forge.ts +53 -0
- package/src/judge-role.ts +160 -0
- package/src/merger-contracts.ts +71 -0
- package/src/merger-git-state.ts +76 -0
- package/src/merger-role.ts +60 -0
- package/src/navigator-attendance.ts +1254 -0
- package/src/navigator-invocation-identity.ts +446 -0
- package/src/open-tool-schema.ts +46 -0
- package/src/package-contracts/collector-output.ts +109 -0
- package/src/package-contracts/fixer-output.ts +81 -0
- package/src/package-contracts/fixer-packet.ts +93 -0
- package/src/package-contracts/judge-output.ts +39 -0
- package/src/package-contracts/reviewer-output.ts +115 -0
- package/src/package-contracts/terminating-tools.ts +259 -0
- package/src/package-contracts/worker-output.ts +36 -0
- package/src/package-owned-tool-idle.ts +134 -0
- package/src/package-resources/method-skill-binding.ts +87 -0
- package/src/package-resources/method-skill.ts +358 -0
- package/src/packaged-role-registry.ts +36 -0
- package/src/public-cli/cli-errors.ts +11 -0
- package/src/public-cli/cli-io.ts +4 -0
- package/src/public-cli/cli.ts +912 -0
- package/src/public-cli/coder-run.ts +575 -0
- package/src/public-cli/collector-run.ts +375 -0
- package/src/public-cli/command-renderer.ts +8 -0
- package/src/public-cli/config.ts +346 -0
- package/src/public-cli/doctor-run.ts +355 -0
- package/src/public-cli/explicit-internal.ts +274 -0
- package/src/public-cli/fixer-run.ts +587 -0
- package/src/public-cli/host-pi-runtime.ts +112 -0
- package/src/public-cli/invocation.ts +1958 -0
- package/src/public-cli/judge-run.ts +507 -0
- package/src/public-cli/main.ts +15 -0
- package/src/public-cli/merger-run.ts +681 -0
- package/src/public-cli/public-run-credentials.ts +71 -0
- package/src/public-cli/registry.ts +153 -0
- package/src/public-cli/reviewer-run.ts +561 -0
- package/src/public-cli/run-lifecycle.ts +884 -0
- package/src/public-cli/settlement.ts +3765 -0
- package/src/public-cli/terminal.ts +325 -0
- package/src/public-command-renderer.ts +43 -0
- package/src/reviewer-agent.ts +94 -0
- package/src/reviewer-auditor.ts +53 -0
- package/src/reviewer-child-executor.ts +31 -0
- package/src/reviewer-construction.ts +137 -0
- package/src/reviewer-dispatch.ts +94 -0
- package/src/reviewer-execution-ledger.ts +206 -0
- package/src/reviewer-failure-diagnostic.ts +18 -0
- package/src/reviewer-git-snapshot.ts +53 -0
- package/src/reviewer-pinned-git.ts +144 -0
- package/src/reviewer-preflight-error.ts +14 -0
- package/src/reviewer-prompt-identity.ts +17 -0
- package/src/reviewer-role.ts +193 -0
- package/src/reviewer-scope-prompt.ts +24 -0
- package/src/reviewer-settlement.ts +63 -0
- package/src/reviewer-workspace.ts +111 -0
- package/src/role-runtime.ts +884 -0
- package/src/sha256.ts +6 -0
- package/src/sitian-record-entry.ts +57 -0
- package/src/stderr-jsonl.ts +28 -0
- package/src/stream-idle-guard.ts +98 -0
- package/src/ticket-snapshot.ts +662 -0
- package/src/ticket-trajectory.ts +1000 -0
- package/src/tool-execution-observation.ts +168 -0
- package/src/uuidv7.ts +1 -0
- package/src/work-subject-identity.ts +94 -0
- package/src/worker-role.ts +434 -0
- package/src/worker-submission-gates.ts +225 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { writeSync } from "node:fs";
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
|
|
5
|
+
import { writeStderrJsonlRecord } from "./stderr-jsonl.ts";
|
|
6
|
+
|
|
7
|
+
/** Mandatory coalesce window for tool_execution_update heartbeats (stderr is bounded). */
|
|
8
|
+
export const TOOL_EXECUTION_UPDATE_THROTTLE_MS = 30_000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Heartbeat basis for downstream silence predicates.
|
|
12
|
+
*
|
|
13
|
+
* Production Pi `bash.js` calls `onUpdate` once with empty content at execute entry,
|
|
14
|
+
* then schedules output-driven updates at ~100 ms. This face skips non-producing
|
|
15
|
+
* (empty-content) updates so the first heartbeat is real child output, then
|
|
16
|
+
* coalesces at {@link TOOL_EXECUTION_UPDATE_THROTTLE_MS}.
|
|
17
|
+
*
|
|
18
|
+
* If a future host path stopped delivering output-driven updates, change this to
|
|
19
|
+
* `"duration-only"` and document that silence predicates degrade to pure duration.
|
|
20
|
+
*/
|
|
21
|
+
export const TOOL_EXECUTION_UPDATE_HEARTBEAT = "output-driven" as const;
|
|
22
|
+
|
|
23
|
+
const observationBase = {
|
|
24
|
+
role: Type.String({ minLength: 1 }),
|
|
25
|
+
toolCallId: Type.String({ minLength: 1 }),
|
|
26
|
+
toolName: Type.String({ minLength: 1 }),
|
|
27
|
+
timestamp: Type.String({ format: "date-time" }),
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export const toolExecutionObservationRecordSchema = Type.Union([
|
|
31
|
+
Type.Object({
|
|
32
|
+
...observationBase,
|
|
33
|
+
event: Type.Literal("tool_execution_start"),
|
|
34
|
+
}, { additionalProperties: true }),
|
|
35
|
+
Type.Object({
|
|
36
|
+
...observationBase,
|
|
37
|
+
event: Type.Literal("tool_execution_update"),
|
|
38
|
+
}, { additionalProperties: true }),
|
|
39
|
+
Type.Object({
|
|
40
|
+
...observationBase,
|
|
41
|
+
event: Type.Literal("tool_execution_end"),
|
|
42
|
+
isError: Type.Boolean(),
|
|
43
|
+
}, { additionalProperties: true }),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
export type ToolExecutionObservationRecord = Static<typeof toolExecutionObservationRecordSchema>;
|
|
47
|
+
export type ToolExecutionObservationWriter = (record: ToolExecutionObservationRecord) => void | Promise<void>;
|
|
48
|
+
|
|
49
|
+
export function validateToolExecutionObservationRecord(record: unknown): ToolExecutionObservationRecord {
|
|
50
|
+
if (!Value.Check(toolExecutionObservationRecordSchema, record)) {
|
|
51
|
+
throw new TypeError("Tool execution observation record does not match its contract");
|
|
52
|
+
}
|
|
53
|
+
return record as ToolExecutionObservationRecord;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function writeToolExecutionObservationRecord(
|
|
57
|
+
record: ToolExecutionObservationRecord,
|
|
58
|
+
write: typeof writeSync = writeSync,
|
|
59
|
+
): void {
|
|
60
|
+
writeStderrJsonlRecord(validateToolExecutionObservationRecord(record), write);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True when a tool_execution_update partialResult carries producing content (not bash's empty entry callback). */
|
|
64
|
+
export function isProducingToolUpdate(partialResult: unknown): boolean {
|
|
65
|
+
if (partialResult == null) return false;
|
|
66
|
+
if (typeof partialResult !== "object") return true;
|
|
67
|
+
const content = (partialResult as { content?: unknown }).content;
|
|
68
|
+
if (!Array.isArray(content)) return true;
|
|
69
|
+
if (content.length === 0) return false;
|
|
70
|
+
return content.some((part) => {
|
|
71
|
+
if (typeof part !== "object" || part === null) return true;
|
|
72
|
+
const text = (part as { text?: unknown }).text;
|
|
73
|
+
if (typeof text === "string") return text.length > 0;
|
|
74
|
+
return true;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type CallObservationState = {
|
|
79
|
+
lastUpdateEmitMonoMs: number | undefined;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type ToolExecutionObservationFace = {
|
|
83
|
+
onStart(event: { toolCallId: string; toolName: string }): void | Promise<void>;
|
|
84
|
+
onUpdate(event: { toolCallId: string; toolName: string; partialResult: unknown }): void | Promise<void>;
|
|
85
|
+
onEnd(event: { toolCallId: string; toolName: string; isError: boolean }): void | Promise<void>;
|
|
86
|
+
reset(): void;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Production throttle clock: genuinely monotonic milliseconds.
|
|
91
|
+
* Wall-clock Date.now() is not safe — NTP/slew can compress two heartbeats under 30s.
|
|
92
|
+
*/
|
|
93
|
+
export function systemToolExecutionObservationMonoNow(): number {
|
|
94
|
+
return performance.now();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createToolExecutionObservationFace(options: {
|
|
98
|
+
role: () => string | undefined;
|
|
99
|
+
admitted: () => boolean;
|
|
100
|
+
clock(): string;
|
|
101
|
+
monoNow(): number;
|
|
102
|
+
write(record: ToolExecutionObservationRecord): void | Promise<void>;
|
|
103
|
+
}): ToolExecutionObservationFace {
|
|
104
|
+
const states = new Map<string, CallObservationState>();
|
|
105
|
+
|
|
106
|
+
async function emit(record: unknown): Promise<void> {
|
|
107
|
+
await options.write(validateToolExecutionObservationRecord(record));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function activeRole(): string | undefined {
|
|
111
|
+
if (!options.admitted()) return undefined;
|
|
112
|
+
const role = options.role();
|
|
113
|
+
return role === undefined || role === "" ? undefined : role;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
reset() {
|
|
118
|
+
states.clear();
|
|
119
|
+
},
|
|
120
|
+
async onStart(event) {
|
|
121
|
+
const role = activeRole();
|
|
122
|
+
if (role === undefined) return;
|
|
123
|
+
states.set(event.toolCallId, { lastUpdateEmitMonoMs: undefined });
|
|
124
|
+
await emit({
|
|
125
|
+
event: "tool_execution_start",
|
|
126
|
+
role,
|
|
127
|
+
toolCallId: event.toolCallId,
|
|
128
|
+
toolName: event.toolName,
|
|
129
|
+
timestamp: options.clock(),
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
async onUpdate(event) {
|
|
133
|
+
const role = activeRole();
|
|
134
|
+
if (role === undefined) return;
|
|
135
|
+
if (!isProducingToolUpdate(event.partialResult)) return;
|
|
136
|
+
const now = options.monoNow();
|
|
137
|
+
const state = states.get(event.toolCallId) ?? { lastUpdateEmitMonoMs: undefined };
|
|
138
|
+
if (
|
|
139
|
+
state.lastUpdateEmitMonoMs !== undefined
|
|
140
|
+
&& now - state.lastUpdateEmitMonoMs < TOOL_EXECUTION_UPDATE_THROTTLE_MS
|
|
141
|
+
) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
state.lastUpdateEmitMonoMs = now;
|
|
145
|
+
states.set(event.toolCallId, state);
|
|
146
|
+
await emit({
|
|
147
|
+
event: "tool_execution_update",
|
|
148
|
+
role,
|
|
149
|
+
toolCallId: event.toolCallId,
|
|
150
|
+
toolName: event.toolName,
|
|
151
|
+
timestamp: options.clock(),
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
async onEnd(event) {
|
|
155
|
+
const role = activeRole();
|
|
156
|
+
if (role === undefined) return;
|
|
157
|
+
states.delete(event.toolCallId);
|
|
158
|
+
await emit({
|
|
159
|
+
event: "tool_execution_end",
|
|
160
|
+
role,
|
|
161
|
+
toolCallId: event.toolCallId,
|
|
162
|
+
toolName: event.toolName,
|
|
163
|
+
timestamp: options.clock(),
|
|
164
|
+
isError: event.isError,
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
package/src/uuidv7.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomBytes}from"node:crypto";const UUIDV7=/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;export function isUuidV7(value:unknown):value is string{return typeof value==="string"&&UUIDV7.test(value)}export function uuidv7(now=Date.now()):string{const b=randomBytes(16);let n=BigInt(now);for(let i=5;i>=0;i--){b[i]=Number(n&255n);n>>=8n}b[6]=(b[6]!&15)|0x70;b[8]=(b[8]!&63)|0x80;const h=b.toString("hex");return`${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single low-level work-subject identity owner.
|
|
3
|
+
* Navigator lifecycle and Terminal settlement both import this module so subject
|
|
4
|
+
* keys cannot drift across a duplicated helper. Depends only on path topology —
|
|
5
|
+
* never the Navigator preparation graph or public-cli bundle surface.
|
|
6
|
+
*/
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
physicalPathIdentity,
|
|
11
|
+
physicallyContainedIn,
|
|
12
|
+
resolveActivationLedgerHome,
|
|
13
|
+
} from "./activation-ledger-topology.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Issue-root segment under `.ak/work/issues/<id>` (optional `#…` suffix stripped).
|
|
17
|
+
* Single low-level owner — Navigator subject helpers must not reimplement this.
|
|
18
|
+
*/
|
|
19
|
+
export function issueRoot(value: string): string | undefined {
|
|
20
|
+
const normalized = value.replaceAll("\\", "/");
|
|
21
|
+
const marker = ".ak/work/issues/";
|
|
22
|
+
const index = normalized.indexOf(marker);
|
|
23
|
+
if (index < 0) return undefined;
|
|
24
|
+
const issue = normalized
|
|
25
|
+
.slice(index + marker.length)
|
|
26
|
+
.split("/")[0]
|
|
27
|
+
?.split("#")[0];
|
|
28
|
+
return issue === undefined || issue === ""
|
|
29
|
+
? undefined
|
|
30
|
+
: normalized.slice(0, index + marker.length) + issue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function workIdentityFromCwd(cwd: string): string | undefined {
|
|
34
|
+
const resolvedCwd = resolve(cwd, ".");
|
|
35
|
+
const cwdIssue = issueRoot(resolvedCwd);
|
|
36
|
+
if (cwdIssue !== undefined) return cwdIssue;
|
|
37
|
+
if (resolvedCwd.includes("/.ak/work/")) return resolvedCwd;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Machine-ledger session paths are not work identity (ADR 0048 session-in-home). */
|
|
42
|
+
function isMachineLedgerSessionPath(sessionPath: string): boolean {
|
|
43
|
+
// Physical containment under the package ledger home — never directory spelling,
|
|
44
|
+
// and stable across macOS /var ↔ /private/var realpath asymmetry.
|
|
45
|
+
return physicallyContainedIn(resolveActivationLedgerHome(), sessionPath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Derive the durable work-subject key from a session directory and cwd.
|
|
50
|
+
* Session placement is an implementation detail; relative and absolute role
|
|
51
|
+
* invocations share one key.
|
|
52
|
+
*/
|
|
53
|
+
export function subjectPath(sessionDir: string, cwd = process.cwd()): string {
|
|
54
|
+
if (sessionDir === "") {
|
|
55
|
+
// Preserve prior fall-through: empty sessionDir with no work cwd → cwd/.ak/work.
|
|
56
|
+
return workIdentityFromCwd(cwd) ?? resolve(cwd, ".ak/work");
|
|
57
|
+
}
|
|
58
|
+
const resolvedSession = resolve(cwd, sessionDir || ".ak/work");
|
|
59
|
+
// Durable role sessions under the machine ledger home are not work roots.
|
|
60
|
+
// Derive subject from cwd (same as empty sessionDir / in-memory) so Navigator
|
|
61
|
+
// keeps issue-root identity when ignition places --session-dir under ADR 0048.
|
|
62
|
+
// Ordinary repository cwd with no explicit work identity uses the established
|
|
63
|
+
// cwd-derived `.ak/work` fallback — never the per-invocation ledger session path.
|
|
64
|
+
if (isMachineLedgerSessionPath(resolvedSession)) {
|
|
65
|
+
return workIdentityFromCwd(cwd) ?? resolve(cwd, ".ak/work");
|
|
66
|
+
}
|
|
67
|
+
const issue = issueRoot(resolvedSession);
|
|
68
|
+
if (issue !== undefined) return issue;
|
|
69
|
+
// Ad-hoc role sessions live below the same work root. Remove the role's
|
|
70
|
+
// private run directory before deriving identity; the run/session spelling
|
|
71
|
+
// must not become a cross-role routing key.
|
|
72
|
+
const runsMarker = "/runs/";
|
|
73
|
+
const runsIndex = resolvedSession.indexOf(runsMarker);
|
|
74
|
+
if (runsIndex >= 0) {
|
|
75
|
+
return resolvedSession.slice(0, runsIndex);
|
|
76
|
+
}
|
|
77
|
+
return resolvedSession;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Project/cwd work identity used by public CLI / empty-sessionDir runs.
|
|
82
|
+
* Equivalent to subjectPath("", projectRoot).
|
|
83
|
+
*/
|
|
84
|
+
export function workSubjectKeyFromProjectRoot(projectRoot: string): string {
|
|
85
|
+
return subjectPath("", projectRoot);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Physical work-subject equality: collapses macOS /var ↔ /private/var (and any
|
|
90
|
+
* other realpath alias) so the same work root correlates across path spellings.
|
|
91
|
+
*/
|
|
92
|
+
export function workSubjectKeysEqual(left: string, right: string): boolean {
|
|
93
|
+
return physicalPathIdentity(left) === physicalPathIdentity(right);
|
|
94
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
AgentToolResult,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Type, type Static } from "typebox";
|
|
8
|
+
import { openToolObjectFromUnion } from "./open-tool-schema.ts";
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
AnyCanonicalSkillBinding,
|
|
12
|
+
CanonicalSkillBinding,
|
|
13
|
+
} from "./canonical-skill-binding.ts";
|
|
14
|
+
import {
|
|
15
|
+
CODER_OUTPUT_TOOL_NAME,
|
|
16
|
+
FIXER_OUTPUT_TOOL_NAME,
|
|
17
|
+
validateAcceptedWorkerDetails,
|
|
18
|
+
type CoderOutput,
|
|
19
|
+
type FixerOutput,
|
|
20
|
+
type WorkerOutput,
|
|
21
|
+
type WorkerRoleLabel,
|
|
22
|
+
} from "./package-contracts/worker-output.ts";
|
|
23
|
+
import { fixerOutputSchema, validateFixerOutput, validateFixerOutputForPacket, type FixerPhase } from "./package-contracts/fixer-output.ts";
|
|
24
|
+
import {
|
|
25
|
+
FixerPacketValidationError,
|
|
26
|
+
parseFixerPrerequisites,
|
|
27
|
+
type FixerInvocationInput,
|
|
28
|
+
} from "./package-contracts/fixer-packet.ts";
|
|
29
|
+
import { createWorkerSubmissionGate } from "./worker-submission-gates.ts";
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
CODER_OUTPUT_TOOL_NAME,
|
|
33
|
+
FIXER_OUTPUT_TOOL_NAME,
|
|
34
|
+
validateAcceptedWorkerDetails,
|
|
35
|
+
};
|
|
36
|
+
export type { WorkerOutput };
|
|
37
|
+
|
|
38
|
+
/** Exact case-sensitive substring literals blocked on Fixer bash only. */
|
|
39
|
+
const FIXER_BASH_FORBIDDEN_LITERALS = [
|
|
40
|
+
"rm -rf",
|
|
41
|
+
"git reset --hard",
|
|
42
|
+
"git clean",
|
|
43
|
+
"git checkout --",
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
function matchFixerBashForbiddenLiteral(
|
|
47
|
+
command: string,
|
|
48
|
+
): (typeof FIXER_BASH_FORBIDDEN_LITERALS)[number] | undefined {
|
|
49
|
+
return FIXER_BASH_FORBIDDEN_LITERALS.find((literal) =>
|
|
50
|
+
command.includes(literal)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const coderOutputVariants = Type.Union([
|
|
55
|
+
Type.Object({
|
|
56
|
+
status: StringEnum(["planned"] as const, { description: "Plan-phase proposal outcome." }),
|
|
57
|
+
report: Type.String({ minLength: 1, description: "Truthful Coder outcome report." }),
|
|
58
|
+
}, { additionalProperties: false }),
|
|
59
|
+
Type.Object({
|
|
60
|
+
status: StringEnum(["completed", "refused"] as const, { description: "Completed or lawfully refused apply outcome." }),
|
|
61
|
+
report: Type.String({ minLength: 1, description: "Truthful Coder outcome report." }),
|
|
62
|
+
}, { additionalProperties: false }),
|
|
63
|
+
Type.Object({
|
|
64
|
+
status: StringEnum(["unfinished"] as const, { description: "Honest unfinished apply outcome." }),
|
|
65
|
+
report: Type.String({ minLength: 1, description: "Truthful Coder outcome report." }),
|
|
66
|
+
remainingScope: Type.String({ minLength: 1, description: "Work remaining after this invocation." }),
|
|
67
|
+
}, { additionalProperties: false }),
|
|
68
|
+
]);
|
|
69
|
+
const coderOutputSchema = openToolObjectFromUnion(coderOutputVariants);
|
|
70
|
+
type WorkerOutputParameters = CoderOutput | FixerOutput;
|
|
71
|
+
export type { FixerOutput, CoderOutput };
|
|
72
|
+
export const FIXER_FLAG_DEFINITIONS = {
|
|
73
|
+
packet: {
|
|
74
|
+
name: "ak-fix-packet",
|
|
75
|
+
definition: {
|
|
76
|
+
description: "Path to opaque prose instructions for the Fixer",
|
|
77
|
+
type: "string" as const,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
prerequisites: {
|
|
81
|
+
name: "ak-fixer-prerequisites",
|
|
82
|
+
definition: {
|
|
83
|
+
description: "Optional path to a JSON array of typed Fixer prerequisites",
|
|
84
|
+
type: "string" as const,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
phase: {
|
|
88
|
+
name: "ak-fixer-phase",
|
|
89
|
+
definition: {
|
|
90
|
+
description:
|
|
91
|
+
"Fixer phase: plan (inspect and propose a repair plan; no edits or commits) or apply (execute the approved plan, verify, and commit when repaired)",
|
|
92
|
+
type: "string" as const,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
} as const;
|
|
96
|
+
|
|
97
|
+
export const FIXER_PHASES = ["plan", "apply"] as const satisfies readonly FixerPhase[];
|
|
98
|
+
type WorkerPhase = (typeof FIXER_PHASES)[number];
|
|
99
|
+
|
|
100
|
+
function isWorkerPhase(value: unknown): value is WorkerPhase {
|
|
101
|
+
return typeof value === "string" && (FIXER_PHASES as readonly string[]).includes(value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type WorkerRoleHostActions = {
|
|
105
|
+
failInfrastructure(error: unknown, ctx: ExtensionContext, toolCallId?: string): never;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type FixerRoleDependencies = {
|
|
109
|
+
loadSoul(): Promise<string>;
|
|
110
|
+
loadPacket(path: string): Promise<string>;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type CoderRoleDependencies = {
|
|
114
|
+
loadSoul(): Promise<string>;
|
|
115
|
+
loadTask(path: string): Promise<string>;
|
|
116
|
+
loadCanonicalSkillBinding?(
|
|
117
|
+
name: "tdd",
|
|
118
|
+
): Promise<AnyCanonicalSkillBinding>;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type WorkerRoleRuntime = {
|
|
122
|
+
activate(ctx?: ExtensionContext): Promise<void>;
|
|
123
|
+
/** Arm gate ① baseline after envelope places the worktree (coder/fixer). Parent feeds sitian durability. */
|
|
124
|
+
armSubmissionGate(cwd: string, parent?: { getSessionFile(): string | undefined }): void;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
128
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function deepFreeze<T>(value: T): T {
|
|
132
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
|
|
133
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
134
|
+
return Object.freeze(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function validateWorkerOutput(
|
|
138
|
+
output: unknown,
|
|
139
|
+
phase: WorkerPhase,
|
|
140
|
+
roleLabel: WorkerRoleLabel,
|
|
141
|
+
): WorkerOutput {
|
|
142
|
+
if (roleLabel === "Fixer") return validateFixerOutput(output, phase);
|
|
143
|
+
return validateAcceptedWorkerDetails(output, "Coder") as CoderOutput;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function requireSingletonSubmissionCall(
|
|
147
|
+
toolCallId: string,
|
|
148
|
+
expectedToolName: string,
|
|
149
|
+
roleLabel: WorkerRoleLabel,
|
|
150
|
+
ctx: ExtensionContext,
|
|
151
|
+
): void {
|
|
152
|
+
const leaf = ctx.sessionManager.getLeafEntry();
|
|
153
|
+
if (leaf?.type !== "message" || leaf.message.role !== "assistant") {
|
|
154
|
+
throw new Error(`${roleLabel} output must be the sole final tool call`);
|
|
155
|
+
}
|
|
156
|
+
const calls = leaf.message.content.filter((part) => part.type === "toolCall");
|
|
157
|
+
const call = calls[0];
|
|
158
|
+
if (
|
|
159
|
+
calls.length !== 1 || call === undefined || call.id !== toolCallId ||
|
|
160
|
+
call.name !== expectedToolName
|
|
161
|
+
) {
|
|
162
|
+
throw new Error(`${roleLabel} output must be the sole final tool call`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createFixerRoleRuntime(
|
|
167
|
+
pi: ExtensionAPI,
|
|
168
|
+
dependencies: FixerRoleDependencies,
|
|
169
|
+
): WorkerRoleRuntime {
|
|
170
|
+
let soul: string | undefined;
|
|
171
|
+
let packet: FixerInvocationInput | undefined;
|
|
172
|
+
let phase: WorkerPhase | undefined;
|
|
173
|
+
let lifecycleRegistered = false;
|
|
174
|
+
const submissionGate = createWorkerSubmissionGate();
|
|
175
|
+
|
|
176
|
+
pi.registerFlag(
|
|
177
|
+
FIXER_FLAG_DEFINITIONS.packet.name,
|
|
178
|
+
FIXER_FLAG_DEFINITIONS.packet.definition,
|
|
179
|
+
);
|
|
180
|
+
pi.registerFlag(
|
|
181
|
+
FIXER_FLAG_DEFINITIONS.prerequisites.name,
|
|
182
|
+
FIXER_FLAG_DEFINITIONS.prerequisites.definition,
|
|
183
|
+
);
|
|
184
|
+
pi.registerFlag(
|
|
185
|
+
FIXER_FLAG_DEFINITIONS.phase.name,
|
|
186
|
+
FIXER_FLAG_DEFINITIONS.phase.definition,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
async activate() {
|
|
191
|
+
soul = (await dependencies.loadSoul()).trim();
|
|
192
|
+
if (soul.length === 0) throw new Error("Fixer soul is empty");
|
|
193
|
+
const selectedPhase = pi.getFlag(FIXER_FLAG_DEFINITIONS.phase.name);
|
|
194
|
+
if (!isWorkerPhase(selectedPhase)) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
"Fixer role requires --ak-fixer-phase plan|apply; no other phase is supported",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
phase = selectedPhase;
|
|
200
|
+
const packetPath = pi.getFlag(FIXER_FLAG_DEFINITIONS.packet.name);
|
|
201
|
+
if (typeof packetPath !== "string" || packetPath.trim().length === 0) {
|
|
202
|
+
throw new Error("Fixer role requires --ak-fix-packet");
|
|
203
|
+
}
|
|
204
|
+
const instructions = await dependencies.loadPacket(packetPath);
|
|
205
|
+
if (instructions.trim().length === 0) {
|
|
206
|
+
throw new FixerPacketValidationError(
|
|
207
|
+
new Error("Fixer instructions must be nonblank"),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const prerequisitesPath = pi.getFlag(FIXER_FLAG_DEFINITIONS.prerequisites.name);
|
|
211
|
+
if (prerequisitesPath !== undefined && (typeof prerequisitesPath !== "string" || prerequisitesPath.trim().length === 0)) {
|
|
212
|
+
throw new Error("Fixer --ak-fixer-prerequisites path must be nonblank when supplied");
|
|
213
|
+
}
|
|
214
|
+
const prerequisites = typeof prerequisitesPath === "string"
|
|
215
|
+
? parseFixerPrerequisites(await dependencies.loadPacket(prerequisitesPath))
|
|
216
|
+
: Object.freeze([]);
|
|
217
|
+
packet = Object.freeze({ instructions, prerequisites });
|
|
218
|
+
|
|
219
|
+
if (!lifecycleRegistered) {
|
|
220
|
+
lifecycleRegistered = true;
|
|
221
|
+
pi.registerTool({
|
|
222
|
+
name: FIXER_OUTPUT_TOOL_NAME,
|
|
223
|
+
label: "Fixer Output",
|
|
224
|
+
description:
|
|
225
|
+
"Submit the plan refusal, apply settlement, or honest unfinished handover.",
|
|
226
|
+
promptSnippet: "Submit the final fixer report",
|
|
227
|
+
promptGuidelines: [
|
|
228
|
+
`Use ${FIXER_OUTPUT_TOOL_NAME} as the final action for the fixer role.`,
|
|
229
|
+
`${FIXER_OUTPUT_TOOL_NAME} reports only lawful assignment blockers; infrastructure failures abort.`,
|
|
230
|
+
"plan permits planned|refused; apply permits completed|refused|partially_completed|unfinished.",
|
|
231
|
+
"When the diff includes test changes, submit testEvidence: contract proven, one-line minimum necessary cost, measured duration.",
|
|
232
|
+
],
|
|
233
|
+
parameters: fixerOutputSchema,
|
|
234
|
+
async execute(toolCallId, parameters, _signal, _onUpdate, ctx): Promise<AgentToolResult<unknown>> {
|
|
235
|
+
if (packet === undefined || phase === undefined) {
|
|
236
|
+
throw new Error("Fixer repair packet and phase were not loaded");
|
|
237
|
+
}
|
|
238
|
+
requireSingletonSubmissionCall(
|
|
239
|
+
toolCallId,
|
|
240
|
+
FIXER_OUTPUT_TOOL_NAME,
|
|
241
|
+
"Fixer",
|
|
242
|
+
ctx,
|
|
243
|
+
);
|
|
244
|
+
const output = deepFreeze(validateFixerOutputForPacket(parameters, phase, packet));
|
|
245
|
+
submissionGate.assertAcceptable(output.status);
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text" as const, text: "Fixer report accepted" }],
|
|
248
|
+
details: output,
|
|
249
|
+
terminate: true as const,
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
pi.on("tool_call", (event) => {
|
|
254
|
+
if (event.toolName !== "bash") return;
|
|
255
|
+
const command = event.input["command"];
|
|
256
|
+
if (typeof command !== "string") return;
|
|
257
|
+
const matched = matchFixerBashForbiddenLiteral(command);
|
|
258
|
+
if (matched === undefined) return;
|
|
259
|
+
return {
|
|
260
|
+
block: true,
|
|
261
|
+
reason:
|
|
262
|
+
`Fixer blocked bash command containing forbidden literal: ${matched}`,
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
pi.on("before_agent_start", (event) => {
|
|
266
|
+
if (soul === undefined) throw new Error("Fixer soul was not loaded");
|
|
267
|
+
return {
|
|
268
|
+
systemPrompt:
|
|
269
|
+
`${event.systemPrompt}\n\n<fixer_soul>\n${soul}\n</fixer_soul>\n\n<fixer_phase>\n${phase ?? ""}\n</fixer_phase>\n\n<fix_packet>\n${packet?.instructions ?? ""}\n</fix_packet>\n\n<fixer_prerequisites>\n${JSON.stringify(packet?.prerequisites ?? [])}\n</fixer_prerequisites>`,
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
armSubmissionGate(cwd: string, parent?: { getSessionFile(): string | undefined }) {
|
|
275
|
+
submissionGate.arm(cwd, parent);
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function createCoderRoleRuntime(
|
|
281
|
+
pi: ExtensionAPI,
|
|
282
|
+
dependencies: CoderRoleDependencies,
|
|
283
|
+
hostActions: WorkerRoleHostActions,
|
|
284
|
+
): WorkerRoleRuntime {
|
|
285
|
+
let soul: string | undefined;
|
|
286
|
+
let task: string | undefined;
|
|
287
|
+
let phase: WorkerPhase | undefined;
|
|
288
|
+
let binding: CanonicalSkillBinding<"tdd"> | undefined;
|
|
289
|
+
let tddInvocationInjected = false;
|
|
290
|
+
let originalRequest: string | undefined;
|
|
291
|
+
let expansionPending = false;
|
|
292
|
+
let expansionCaptured = false;
|
|
293
|
+
let lifecycleRegistered = false;
|
|
294
|
+
const submissionGate = createWorkerSubmissionGate();
|
|
295
|
+
|
|
296
|
+
pi.registerFlag("ak-coder-task", {
|
|
297
|
+
description: "Markdown task assigned to the coder role",
|
|
298
|
+
type: "string",
|
|
299
|
+
});
|
|
300
|
+
pi.registerFlag("ak-coder-phase", {
|
|
301
|
+
description:
|
|
302
|
+
"Coder phase: plan (inspect and propose an implementation plan; no edits or commits) or apply (execute the approved plan and verify the first implementation)",
|
|
303
|
+
type: "string",
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
async activate(ctx) {
|
|
308
|
+
soul = (await dependencies.loadSoul()).trim();
|
|
309
|
+
if (soul.length === 0) throw new Error("Coder soul is empty");
|
|
310
|
+
const selectedPhase = pi.getFlag("ak-coder-phase");
|
|
311
|
+
if (selectedPhase !== "plan" && selectedPhase !== "apply") {
|
|
312
|
+
throw new Error(
|
|
313
|
+
"Coder role requires --ak-coder-phase plan|apply; no other phase is supported",
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
phase = selectedPhase;
|
|
317
|
+
const taskPath = pi.getFlag("ak-coder-task");
|
|
318
|
+
if (typeof taskPath !== "string" || taskPath.trim().length === 0) {
|
|
319
|
+
throw new Error("Coder role requires --ak-coder-task");
|
|
320
|
+
}
|
|
321
|
+
task = (await dependencies.loadTask(taskPath)).trim();
|
|
322
|
+
if (task.length === 0) throw new Error("Coder task is empty");
|
|
323
|
+
binding = undefined;
|
|
324
|
+
if (phase === "apply") {
|
|
325
|
+
if (dependencies.loadCanonicalSkillBinding === undefined) {
|
|
326
|
+
throw new Error("Coder canonical Skill binding loader is not configured");
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const loaded = await dependencies.loadCanonicalSkillBinding("tdd");
|
|
330
|
+
if (loaded.name !== "tdd") {
|
|
331
|
+
throw new Error(
|
|
332
|
+
"Canonical Skill binding loader returned code-review for tdd",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
binding = loaded;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (ctx === undefined) throw error;
|
|
338
|
+
hostActions.failInfrastructure(error, ctx);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!lifecycleRegistered) {
|
|
343
|
+
lifecycleRegistered = true;
|
|
344
|
+
pi.registerTool({
|
|
345
|
+
name: CODER_OUTPUT_TOOL_NAME,
|
|
346
|
+
label: "Coder Output",
|
|
347
|
+
description:
|
|
348
|
+
"Submit a plan, completion, unfinished handoff, or evidence-bearing refusal for the active coder phase.",
|
|
349
|
+
promptSnippet: "Submit the final coder report",
|
|
350
|
+
promptGuidelines: [
|
|
351
|
+
`Use ${CODER_OUTPUT_TOOL_NAME} as the final action for the coder role.`,
|
|
352
|
+
`${CODER_OUTPUT_TOOL_NAME} never escalates; explain authority or task conflicts in report for the caller to dispose.`,
|
|
353
|
+
"plan permits planned|refused; apply permits completed|unfinished|refused. unfinished requires a non-blank remainingScope.",
|
|
354
|
+
"A completed apply report must preserve evidence for TDD, the same-pattern check, introduced-regression check, and behavior-fact check.",
|
|
355
|
+
],
|
|
356
|
+
parameters: coderOutputSchema,
|
|
357
|
+
async execute(toolCallId, parameters, _signal, _onUpdate, ctx) {
|
|
358
|
+
if (task === undefined || phase === undefined) {
|
|
359
|
+
throw new Error("Coder task and phase were not loaded");
|
|
360
|
+
}
|
|
361
|
+
requireSingletonSubmissionCall(
|
|
362
|
+
toolCallId,
|
|
363
|
+
CODER_OUTPUT_TOOL_NAME,
|
|
364
|
+
"Coder",
|
|
365
|
+
ctx,
|
|
366
|
+
);
|
|
367
|
+
const output = validateWorkerOutput(parameters, phase, "Coder");
|
|
368
|
+
if (
|
|
369
|
+
phase === "apply" && output.status === "completed" &&
|
|
370
|
+
!expansionCaptured
|
|
371
|
+
) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
"Coder completed requires the Matt tdd skill to be expanded through Pi /skill:tdd",
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
submissionGate.assertAcceptable(output.status);
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: "text" as const, text: "Coder report accepted" }],
|
|
379
|
+
details: output,
|
|
380
|
+
terminate: true as const,
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
pi.on("input", (event) => {
|
|
385
|
+
if (phase !== "apply" || tddInvocationInjected) {
|
|
386
|
+
return { action: "continue" as const };
|
|
387
|
+
}
|
|
388
|
+
tddInvocationInjected = true;
|
|
389
|
+
expansionPending = true;
|
|
390
|
+
const isNativeTdd =
|
|
391
|
+
event.text === "/skill:tdd" ||
|
|
392
|
+
event.text.startsWith("/skill:tdd ");
|
|
393
|
+
if (isNativeTdd) {
|
|
394
|
+
originalRequest = event.text.slice("/skill:tdd".length).trim();
|
|
395
|
+
return { action: "continue" as const };
|
|
396
|
+
}
|
|
397
|
+
originalRequest = event.text.trim();
|
|
398
|
+
return {
|
|
399
|
+
action: "transform" as const,
|
|
400
|
+
text: binding?.invocation(event.text) ?? `/skill:tdd ${event.text}`,
|
|
401
|
+
...(event.images === undefined ? {} : { images: event.images }),
|
|
402
|
+
};
|
|
403
|
+
});
|
|
404
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
405
|
+
if (soul === undefined) throw new Error("Coder soul was not loaded");
|
|
406
|
+
if (phase === "apply") {
|
|
407
|
+
if (binding === undefined) {
|
|
408
|
+
hostActions.failInfrastructure(
|
|
409
|
+
new Error("Coder canonical tdd Skill binding was not initialized"),
|
|
410
|
+
ctx,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (expansionPending) {
|
|
414
|
+
expansionPending = false;
|
|
415
|
+
if (originalRequest !== undefined) {
|
|
416
|
+
expansionCaptured = binding.captureExpansion(
|
|
417
|
+
event.prompt,
|
|
418
|
+
originalRequest,
|
|
419
|
+
) !== undefined;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
systemPrompt:
|
|
425
|
+
`${event.systemPrompt}\n\n<coder_soul>\n${soul}\n</coder_soul>\n\n<coder_phase>\n${phase ?? ""}\n</coder_phase>\n\n<coder_task>\n${task ?? ""}\n</coder_task>`,
|
|
426
|
+
};
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
armSubmissionGate(cwd: string, parent?: { getSessionFile(): string | undefined }) {
|
|
431
|
+
submissionGate.arm(cwd, parent);
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|