@akagilnc/pi-workflow-roles 0.1.5797 → 0.1.5820
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/dist/acp-host/production-host.js +464 -355
- package/dist/acp-host/role-turn-host.js +484 -0
- package/dist/activation-ledger-session.js +105 -0
- package/dist/activation-ledger.js +8 -0
- package/dist/activation-trace.js +33 -0
- package/dist/audit-escalation.js +7 -0
- package/dist/audited-run-identity.js +21 -0
- package/dist/auditor-role.js +7 -0
- package/dist/book-topology-migration-placement.js +404 -0
- package/dist/book-topology-migration.js +125 -0
- package/dist/book-topology-migrators-t11.js +677 -0
- package/dist/book-topology-mixed-volume-migrators.js +322 -0
- package/dist/book-topology-partition-migrators.js +23 -0
- package/dist/book-topology-record-class-migrators.js +671 -0
- package/dist/book-topology-runs-migrator.js +272 -0
- package/dist/collector-role.js +109 -0
- package/dist/compliance-transport.js +5 -3
- package/dist/countersign-role.js +17 -0
- package/dist/diarist-role.js +8 -0
- package/dist/diarist.js +15 -0
- package/dist/doctor-role.js +80 -0
- package/dist/engine-detour-tool.js +51 -37
- package/dist/engine-detour.js +22 -11
- package/dist/factory-board.js +1478 -0
- package/dist/fixer-bash-seatbelt.js +13 -0
- package/dist/gatekeeper-role.js +1 -2
- package/dist/gh-api-runner.js +81 -0
- package/dist/gleaner-left-role.js +8 -0
- package/dist/headless-host/production-host.js +464 -355
- package/dist/headless-host/role-turn-host.js +691 -0
- package/dist/human-format.js +67 -0
- package/dist/inspector-role.js +12 -0
- package/dist/judge-recording-anti-forge.js +39 -0
- package/dist/judge-role.js +21 -3
- package/dist/merger-role.js +57 -0
- package/dist/migrate-book-topology.js +0 -2
- package/dist/navigator-role.js +6 -0
- package/dist/navigator-work-context.js +72 -0
- package/dist/notary-role.js +113 -0
- package/dist/oauth-keepalive.js +149 -0
- package/dist/package-contracts/terminating-infrastructure.js +0 -35
- package/dist/pi/adapter.js +272 -0
- package/dist/public-cli/cli.js +9 -3
- package/dist/public-cli/instruction-seat-run.js +163 -44
- package/dist/public-cli/main.js +241 -123
- package/dist/report-ticket-tool.js +24 -13
- package/dist/reviewer-role.js +33 -0
- package/dist/role-envelope.js +707 -0
- package/dist/role-runtime-dependencies.js +57 -0
- package/dist/role-runtime.js +1476 -0
- package/dist/secretariat-role.js +5 -0
- package/dist/stderr-jsonl.js +26 -0
- package/dist/submission-correctable-error.js +2 -6
- package/dist/submission-errors.js +0 -30
- package/dist/submission-gate.js +61 -54
- package/dist/submission-ledger.js +105 -98
- package/dist/ticket-snapshot.js +486 -0
- package/dist/ticket-trajectory.js +843 -0
- package/dist/worker-role.js +184 -0
- package/dist/worker-submission-gates.js +333 -0
- package/package.json +1 -1
- package/resources/engine-dispatch.md +6 -6
- package/resources/engines/agy.md +1 -1
- package/src/compliance-transport.ts +7 -3
- package/src/engine-detour-tool.ts +88 -57
- package/src/engine-detour.ts +31 -14
- package/src/gatekeeper-role.ts +1 -15
- package/src/host-contracts.ts +9 -12
- package/src/judge-role.ts +30 -7
- package/src/package-contracts/terminating-infrastructure.ts +8 -81
- package/src/pi/adapter.ts +0 -1
- package/src/public-cli/cli.ts +21 -14
- package/src/public-cli/instruction-seat-run.ts +227 -44
- package/src/report-ticket-tool.ts +27 -13
- package/src/role-envelope.ts +35 -35
- package/src/role-runtime.ts +63 -56
- package/src/submission-correctable-error.ts +1 -6
- package/src/submission-errors.ts +0 -34
- package/src/submission-gate.ts +71 -56
- package/src/submission-ledger.ts +109 -104
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { validateAcceptedWorkerDetails, } from "./package-contracts/worker-output.js";
|
|
2
|
+
import { validateFixerOutput } from "./package-contracts/fixer-output.js";
|
|
3
|
+
import { registerFiledSubmissionTool } from "./filed-submission.js";
|
|
4
|
+
import { roleSubmissionDeclaration } from "./role-submission-declarations.js";
|
|
5
|
+
import { createWorkerSubmissionGate, WorkerCommitReminderError, WorkerPrefixReminderError, WorkerUnfinishedReasonReminderError, } from "./worker-submission-gates.js";
|
|
6
|
+
import { deliveryLimitFromEnv } from "./receipt-delivery-policy.js";
|
|
7
|
+
import { fixerBashSeatbeltDenyReason, matchFixerBashForbiddenLiteral, } from "./fixer-bash-seatbelt.js";
|
|
8
|
+
import { isRecord } from "./unknown-value.js";
|
|
9
|
+
export { CODER_OUTPUT_TOOL_NAME, FIXER_OUTPUT_TOOL_NAME, coderOutputSchema, validateAcceptedWorkerDetails, } from "./package-contracts/worker-output.js";
|
|
10
|
+
/** #1168: phase only — dispatch text is the first message, not a packet flag. */
|
|
11
|
+
export const FIXER_FLAG_DEFINITIONS = {
|
|
12
|
+
phase: {
|
|
13
|
+
name: "ak-fixer-phase",
|
|
14
|
+
definition: {
|
|
15
|
+
description: "Fixer phase: plan (inspect and propose a repair plan; no edits or commits) or apply (execute the approved plan, verify, and commit when repaired)",
|
|
16
|
+
type: "string",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export const FIXER_PHASES = ["plan", "apply"];
|
|
21
|
+
function isWorkerPhase(value) {
|
|
22
|
+
return typeof value === "string" && FIXER_PHASES.includes(value);
|
|
23
|
+
}
|
|
24
|
+
/** Read only the field that selects the worker's next package-owned path. */
|
|
25
|
+
function workerStatusOf(output) {
|
|
26
|
+
return isRecord(output) && typeof output.status === "string"
|
|
27
|
+
? output.status
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
function deepFreeze(value) {
|
|
31
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value))
|
|
32
|
+
return value;
|
|
33
|
+
for (const child of Object.values(value))
|
|
34
|
+
deepFreeze(child);
|
|
35
|
+
return Object.freeze(value);
|
|
36
|
+
}
|
|
37
|
+
export function validateWorkerOutput(output, phase, roleLabel) {
|
|
38
|
+
if (roleLabel === "Fixer")
|
|
39
|
+
return validateFixerOutput(output, phase);
|
|
40
|
+
return validateAcceptedWorkerDetails(output, "Coder");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Coder and fixer share one submission sequence: record via the shared tool,
|
|
44
|
+
* then read status and bounce through the same reminder gate.
|
|
45
|
+
* Seat differences stay in the ready check and the projection.
|
|
46
|
+
*/
|
|
47
|
+
function registerWorkerSubmission(pi, role, options) {
|
|
48
|
+
registerFiledSubmissionTool(pi, roleSubmissionDeclaration(role), {
|
|
49
|
+
readyError: () => (options.ready() ? undefined : options.notReady),
|
|
50
|
+
beforeAccept: async ({ toolCallId, parameters, ctx }) => {
|
|
51
|
+
const phase = options.phase();
|
|
52
|
+
if (phase === undefined)
|
|
53
|
+
throw new Error(options.notReady);
|
|
54
|
+
const output = options.project(parameters, phase);
|
|
55
|
+
const status = workerStatusOf(output);
|
|
56
|
+
if (status !== undefined) {
|
|
57
|
+
assertAcceptableThroughHost(options.submissionGate, status, output, options.hostActions, ctx, toolCallId);
|
|
58
|
+
}
|
|
59
|
+
return output;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Reminder bounces stay typed rejects; IO/infrastructure keep identity via host failInfrastructure. */
|
|
64
|
+
function assertAcceptableThroughHost(submissionGate, status, details, hostActions, ctx, toolCallId) {
|
|
65
|
+
try {
|
|
66
|
+
submissionGate.assertAcceptable(status, details);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof WorkerCommitReminderError ||
|
|
70
|
+
error instanceof WorkerPrefixReminderError ||
|
|
71
|
+
error instanceof WorkerUnfinishedReasonReminderError) {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
hostActions.failInfrastructure(error, ctx, toolCallId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function createFixerRoleRuntime(pi, dependencies, hostActions, options) {
|
|
78
|
+
let soul;
|
|
79
|
+
let phase;
|
|
80
|
+
let lifecycleRegistered = false;
|
|
81
|
+
// #1132: ADR 0050 缺理由催全次数. The in-process seam passes the value it
|
|
82
|
+
// already resolved; the Pi child has only the env that seam projected.
|
|
83
|
+
const submissionGate = createWorkerSubmissionGate({
|
|
84
|
+
unfinishedReasonBounceLimit: options?.unfinishedReasonBounceLimit
|
|
85
|
+
?? deliveryLimitFromEnv(process.env),
|
|
86
|
+
});
|
|
87
|
+
pi.registerFlag(FIXER_FLAG_DEFINITIONS.phase.name, FIXER_FLAG_DEFINITIONS.phase.definition);
|
|
88
|
+
return {
|
|
89
|
+
async activate() {
|
|
90
|
+
soul = (await dependencies.loadSoul()).trim();
|
|
91
|
+
if (soul.length === 0)
|
|
92
|
+
throw new Error("Fixer soul is empty");
|
|
93
|
+
const selectedPhase = pi.getFlag(FIXER_FLAG_DEFINITIONS.phase.name);
|
|
94
|
+
if (!isWorkerPhase(selectedPhase)) {
|
|
95
|
+
throw new Error("Fixer role requires --ak-fixer-phase plan|apply; no other phase is supported");
|
|
96
|
+
}
|
|
97
|
+
phase = selectedPhase;
|
|
98
|
+
if (!lifecycleRegistered) {
|
|
99
|
+
lifecycleRegistered = true;
|
|
100
|
+
registerWorkerSubmission(pi, "fixer", {
|
|
101
|
+
notReady: "修内司阶段未装载",
|
|
102
|
+
ready: () => phase !== undefined,
|
|
103
|
+
phase: () => phase,
|
|
104
|
+
project: (parameters, current) => deepFreeze(validateFixerOutput(parameters, current)),
|
|
105
|
+
submissionGate,
|
|
106
|
+
hostActions,
|
|
107
|
+
});
|
|
108
|
+
pi.on("tool_call", (event) => {
|
|
109
|
+
if (event.toolName !== "bash")
|
|
110
|
+
return;
|
|
111
|
+
const command = event.input["command"];
|
|
112
|
+
if (typeof command !== "string")
|
|
113
|
+
return;
|
|
114
|
+
const matched = matchFixerBashForbiddenLiteral(command);
|
|
115
|
+
if (matched === undefined)
|
|
116
|
+
return;
|
|
117
|
+
return {
|
|
118
|
+
block: true,
|
|
119
|
+
reason: fixerBashSeatbeltDenyReason(matched),
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
// #1168: soul + phase only; dispatch text is the first message.
|
|
123
|
+
pi.on("before_agent_start", (event) => {
|
|
124
|
+
if (soul === undefined)
|
|
125
|
+
throw new Error("修内司职分未装载");
|
|
126
|
+
return {
|
|
127
|
+
systemPrompt: `${event.systemPrompt}\n\n<fixer_soul>\n${soul}\n</fixer_soul>\n\n<fixer_phase>\n${phase ?? ""}\n</fixer_phase>`,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
armSubmissionGate(cwd, parent, invocationScopeId) {
|
|
133
|
+
submissionGate.arm(cwd, parent, invocationScopeId);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export function createCoderRoleRuntime(pi, dependencies, hostActions, options) {
|
|
138
|
+
let soul;
|
|
139
|
+
let phase;
|
|
140
|
+
let lifecycleRegistered = false;
|
|
141
|
+
// #1132: same resolved ceiling as the fixer gate. Absent = Pi child env.
|
|
142
|
+
const submissionGate = createWorkerSubmissionGate({
|
|
143
|
+
unfinishedReasonBounceLimit: options?.unfinishedReasonBounceLimit
|
|
144
|
+
?? deliveryLimitFromEnv(process.env),
|
|
145
|
+
});
|
|
146
|
+
pi.registerFlag("ak-coder-phase", {
|
|
147
|
+
description: "Coder phase: plan (inspect and propose an implementation plan; no edits or commits) or apply (execute the approved plan and verify the first implementation)",
|
|
148
|
+
type: "string",
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
async activate() {
|
|
152
|
+
soul = (await dependencies.loadSoul()).trim();
|
|
153
|
+
if (soul.length === 0)
|
|
154
|
+
throw new Error("Coder soul is empty");
|
|
155
|
+
const selectedPhase = pi.getFlag("ak-coder-phase");
|
|
156
|
+
if (selectedPhase !== "plan" && selectedPhase !== "apply") {
|
|
157
|
+
throw new Error("Coder role requires --ak-coder-phase plan|apply; no other phase is supported");
|
|
158
|
+
}
|
|
159
|
+
phase = selectedPhase;
|
|
160
|
+
if (!lifecycleRegistered) {
|
|
161
|
+
lifecycleRegistered = true;
|
|
162
|
+
registerWorkerSubmission(pi, "coder", {
|
|
163
|
+
notReady: "将作监阶段未装载",
|
|
164
|
+
ready: () => phase !== undefined,
|
|
165
|
+
phase: () => phase,
|
|
166
|
+
project: (parameters, current) => validateWorkerOutput(parameters, current, "Coder"),
|
|
167
|
+
submissionGate,
|
|
168
|
+
hostActions,
|
|
169
|
+
});
|
|
170
|
+
// #1168: soul + phase only; dispatch text is the first message.
|
|
171
|
+
pi.on("before_agent_start", (event) => {
|
|
172
|
+
if (soul === undefined)
|
|
173
|
+
throw new Error("将作监职分未装载");
|
|
174
|
+
return {
|
|
175
|
+
systemPrompt: `${event.systemPrompt}\n\n<coder_soul>\n${soul}\n</coder_soul>\n\n<coder_phase>\n${phase ?? ""}\n</coder_phase>`,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
armSubmissionGate(cwd, parent, invocationScopeId) {
|
|
181
|
+
submissionGate.arm(cwd, parent, invocationScopeId);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/** #242/#369 worker gates ①② at submission seam.
|
|
2
|
+
* Durability (#1178): this leg's state.jsonl via reportRunRecord / readStateControlRowsSync.
|
|
3
|
+
* No session/worker-submission-gate/ side-branch nest.
|
|
4
|
+
*/
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, rmdirSync, rmSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { runDirectoryOfSessionFile } from "./role-run-placement.js";
|
|
9
|
+
import { readStateControlRowsSync } from "./run-dossier.js";
|
|
10
|
+
import { RUN_STATE_FILE } from "./run-dossier-files.js";
|
|
11
|
+
import { reportRunRecord } from "./sitian-facade.js";
|
|
12
|
+
import { WorkerCommitReminderError, WorkerPrefixReminderError, WorkerUnfinishedReasonReminderError, } from "./submission-errors.js";
|
|
13
|
+
import { WORKER_DONE_STATUSES } from "./worker-submission-contracts.js";
|
|
14
|
+
import { deliveryLimitFromConfig } from "./receipt-delivery-policy.js";
|
|
15
|
+
import { isRecord } from "./unknown-value.js";
|
|
16
|
+
export { WorkerCommitReminderError, WorkerPrefixReminderError, WorkerUnfinishedReasonReminderError } from "./submission-errors.js";
|
|
17
|
+
export { WORKER_DONE_STATUSES } from "./worker-submission-contracts.js";
|
|
18
|
+
export const WORKER_COMMIT_BASELINE_ENTRY_TYPE = "commit-baseline";
|
|
19
|
+
export const WORKER_COMMIT_REMINDER_BOUNCE_ENTRY_TYPE = "commit-reminder-bounce";
|
|
20
|
+
export const WORKER_PREFIX_REMINDER_BOUNCE_ENTRY_TYPE = "prefix-reminder-bounce";
|
|
21
|
+
export const WORKER_UNFINISHED_REASON_BOUNCE_ENTRY_TYPE = "unfinished-reason-bounce";
|
|
22
|
+
/** Historical package hook ownership marker — uninstall criterion only. */
|
|
23
|
+
const HOOK_MARKER = "ak-roles: worker-submission-gates reference-transaction";
|
|
24
|
+
const HOOKS_DIR = "ak-roles-hooks";
|
|
25
|
+
const HOOK_FILE = "reference-transaction";
|
|
26
|
+
/** Open platform-prefix domain (constitution #10) — not a closed singleton. */
|
|
27
|
+
const PLATFORM_PREFIX = /^[A-Za-z][A-Za-z0-9_-]*:/;
|
|
28
|
+
const GATE_SOURCE = "worker-submission-gates";
|
|
29
|
+
function git(cwd, args) {
|
|
30
|
+
return execFileSync("git", args, {
|
|
31
|
+
cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
|
32
|
+
env: { ...process.env, GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_COMMON_DIR: undefined },
|
|
33
|
+
}).trim();
|
|
34
|
+
}
|
|
35
|
+
function gitFile(file, args) {
|
|
36
|
+
return execFileSync("git", ["config", "--file", file, ...args], {
|
|
37
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
|
38
|
+
env: { ...process.env, GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_COMMON_DIR: undefined },
|
|
39
|
+
}).trim();
|
|
40
|
+
}
|
|
41
|
+
function statusOf(error) {
|
|
42
|
+
return typeof error === "object" && error !== null && "status" in error
|
|
43
|
+
? error.status
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
function tryGetAll(file, key) {
|
|
47
|
+
if (!existsSync(file))
|
|
48
|
+
return [];
|
|
49
|
+
try {
|
|
50
|
+
const out = gitFile(file, ["--get-all", key]);
|
|
51
|
+
return out.length === 0 ? [] : out.split("\n");
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// --get-all exit 1 = absent; other failures stay loud.
|
|
55
|
+
if (statusOf(error) !== 1)
|
|
56
|
+
throw error;
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** True only when the file exists and carries the historical package marker.
|
|
61
|
+
* Read failures propagate — never disguised as "not owned". */
|
|
62
|
+
function ownedHook(path) {
|
|
63
|
+
if (!existsSync(path))
|
|
64
|
+
return false;
|
|
65
|
+
return readFileSync(path, "utf8").includes(HOOK_MARKER);
|
|
66
|
+
}
|
|
67
|
+
/** Escape a hooksPath value for git config --unset value-pattern (POSIX ERE). */
|
|
68
|
+
function escapeGitConfigValueRegex(value) {
|
|
69
|
+
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Remove only package-owned core.hooksPath values; keep every foreign value.
|
|
73
|
+
* --unset without a value-pattern exits 5 for both "absent" and "multi-value",
|
|
74
|
+
* so multi-valued keys must be addressed per matching value.
|
|
75
|
+
*/
|
|
76
|
+
function unsetOwnedHooksPath(file) {
|
|
77
|
+
const owned = [];
|
|
78
|
+
for (const value of tryGetAll(file, "core.hooksPath")) {
|
|
79
|
+
if (!ownedHook(resolve(value, HOOK_FILE)))
|
|
80
|
+
continue;
|
|
81
|
+
try {
|
|
82
|
+
// --unset-all + exact value-pattern drops every duplicate owned copy; foreign stays.
|
|
83
|
+
gitFile(file, [
|
|
84
|
+
"--unset-all",
|
|
85
|
+
"core.hooksPath",
|
|
86
|
+
`^${escapeGitConfigValueRegex(value)}$`,
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// 5 = this specific value already absent (not multi-value ambiguity).
|
|
91
|
+
if (statusOf(error) !== 5)
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
owned.push(value);
|
|
95
|
+
}
|
|
96
|
+
return owned;
|
|
97
|
+
}
|
|
98
|
+
/** Delete only the package-owned hook file; rmdir solely when empty. */
|
|
99
|
+
function rmOwnedDir(dir) {
|
|
100
|
+
const hookPath = resolve(dir, HOOK_FILE);
|
|
101
|
+
if (!ownedHook(hookPath))
|
|
102
|
+
return;
|
|
103
|
+
rmSync(hookPath, { force: true });
|
|
104
|
+
if (existsSync(dir) && readdirSync(dir).length === 0)
|
|
105
|
+
rmdirSync(dir);
|
|
106
|
+
}
|
|
107
|
+
function linkedGitDirs(commonDir) {
|
|
108
|
+
const root = resolve(commonDir, "worktrees");
|
|
109
|
+
if (!existsSync(root))
|
|
110
|
+
return [];
|
|
111
|
+
// Enumeration/lstat failures propagate — never skip a linked admin dir silently.
|
|
112
|
+
// lstat does not follow: symlink entries are not directories and stay out of range.
|
|
113
|
+
return readdirSync(root)
|
|
114
|
+
.map((name) => resolve(root, name))
|
|
115
|
+
.filter((dir) => lstatSync(dir).isDirectory());
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* ADR 0070 §4 — private one-shot uninstall on arm.
|
|
119
|
+
* Range: current repo + enumerable worktree admin dirs. Owned hooksPath/files only.
|
|
120
|
+
* Never rolls back extensions.worktreeConfig / migrated bare|worktree / foreign hooksPath.
|
|
121
|
+
*/
|
|
122
|
+
function uninstallPackageWorkerHooks(cwd) {
|
|
123
|
+
let inside;
|
|
124
|
+
try {
|
|
125
|
+
inside = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (inside !== "true")
|
|
131
|
+
return;
|
|
132
|
+
const commonDir = git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
133
|
+
// Unset owned hooksPath before deleting the marker file (ownership check needs it).
|
|
134
|
+
const clear = (configFile) => {
|
|
135
|
+
for (const hooks of unsetOwnedHooksPath(configFile))
|
|
136
|
+
rmOwnedDir(hooks);
|
|
137
|
+
};
|
|
138
|
+
clear(resolve(commonDir, "config"));
|
|
139
|
+
clear(resolve(commonDir, "config.worktree"));
|
|
140
|
+
rmOwnedDir(resolve(commonDir, HOOKS_DIR));
|
|
141
|
+
const legacy = resolve(commonDir, "hooks", HOOK_FILE);
|
|
142
|
+
if (ownedHook(legacy))
|
|
143
|
+
rmSync(legacy, { force: true });
|
|
144
|
+
for (const gitDir of linkedGitDirs(commonDir)) {
|
|
145
|
+
clear(resolve(gitDir, "config.worktree"));
|
|
146
|
+
rmOwnedDir(resolve(gitDir, HOOKS_DIR));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** ADR 0050: a written explanation, including text carried by an object key. */
|
|
150
|
+
function containsWrittenReason(value) {
|
|
151
|
+
if (typeof value === "string")
|
|
152
|
+
return value.trim().length > 0;
|
|
153
|
+
if (Array.isArray(value))
|
|
154
|
+
return value.some(containsWrittenReason);
|
|
155
|
+
if (isRecord(value)) {
|
|
156
|
+
return Object.entries(value).some(([key, item]) => containsWrittenReason(key) || containsWrittenReason(item));
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
function unfinishedReasonPresent(details) {
|
|
161
|
+
if (!isRecord(details))
|
|
162
|
+
return false;
|
|
163
|
+
return containsWrittenReason(details.reason);
|
|
164
|
+
}
|
|
165
|
+
function liveRunDirectory(parent) {
|
|
166
|
+
const file = parent.getSessionFile();
|
|
167
|
+
if (file === undefined || file.length === 0) {
|
|
168
|
+
throw new Error("Worker submission gate requires a parent session file for this leg's state.jsonl");
|
|
169
|
+
}
|
|
170
|
+
return runDirectoryOfSessionFile(file);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Interpret gate facts from this leg's control-plane state rows.
|
|
174
|
+
* File IO / decoder fail-closed is sole-owned by readStateControlRowsSync;
|
|
175
|
+
* illegal gate payloads still refuse here — never wash into re-arm (#1178 C1/C4).
|
|
176
|
+
*/
|
|
177
|
+
function readGateState(runDirectory, invocationScopeId) {
|
|
178
|
+
const rows = readStateControlRowsSync(runDirectory);
|
|
179
|
+
let baseline;
|
|
180
|
+
let reminded = false;
|
|
181
|
+
let prefixReminded = false;
|
|
182
|
+
let unfinishedReasonBounces = 0;
|
|
183
|
+
for (const row of rows) {
|
|
184
|
+
const kind = row.kind;
|
|
185
|
+
const payload = row.payload;
|
|
186
|
+
if (kind === WORKER_COMMIT_BASELINE_ENTRY_TYPE) {
|
|
187
|
+
// Illegal payload keeps failure identity — never wash into older/absent baseline.
|
|
188
|
+
if (!isRecord(payload) || !(payload.head === null || typeof payload.head === "string")) {
|
|
189
|
+
throw new Error(`${RUN_STATE_FILE} ${WORKER_COMMIT_BASELINE_ENTRY_TYPE} payload is invalid: ${runDirectory}`);
|
|
190
|
+
}
|
|
191
|
+
baseline = payload.head;
|
|
192
|
+
}
|
|
193
|
+
else if (kind === WORKER_COMMIT_REMINDER_BOUNCE_ENTRY_TYPE) {
|
|
194
|
+
reminded = true;
|
|
195
|
+
}
|
|
196
|
+
else if (kind === WORKER_PREFIX_REMINDER_BOUNCE_ENTRY_TYPE) {
|
|
197
|
+
prefixReminded = true;
|
|
198
|
+
}
|
|
199
|
+
else if (kind === WORKER_UNFINISHED_REASON_BOUNCE_ENTRY_TYPE) {
|
|
200
|
+
if (!isRecord(payload)) {
|
|
201
|
+
throw new Error(`${RUN_STATE_FILE} ${WORKER_UNFINISHED_REASON_BOUNCE_ENTRY_TYPE} payload is invalid: ${runDirectory}`);
|
|
202
|
+
}
|
|
203
|
+
if (invocationScopeId !== undefined && payload.invocationScopeId === invocationScopeId) {
|
|
204
|
+
unfinishedReasonBounces += 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { baseline, reminded, prefixReminded, unfinishedReasonBounces };
|
|
209
|
+
}
|
|
210
|
+
function appendGateState(runDirectory, kind, payload) {
|
|
211
|
+
reportRunRecord(runDirectory, kind, payload, GATE_SOURCE);
|
|
212
|
+
}
|
|
213
|
+
function isAncestor(cwd, ancestor, descendant) {
|
|
214
|
+
try {
|
|
215
|
+
git(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (statusOf(error) === 1)
|
|
220
|
+
return false;
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Reliable window (ADR 0070). null = unreliable tip-SHA baseline; [] = empty.
|
|
226
|
+
* Structured git-log fields only — never parse a shell command string.
|
|
227
|
+
*/
|
|
228
|
+
function reliableWindow(cwd, baseline, head) {
|
|
229
|
+
if (baseline !== null && !isAncestor(cwd, baseline, head))
|
|
230
|
+
return null;
|
|
231
|
+
const range = baseline === null ? head : `${baseline}..${head}`;
|
|
232
|
+
const raw = git(cwd, ["log", "--format=%P%x1e%s", range]);
|
|
233
|
+
if (raw.length === 0)
|
|
234
|
+
return [];
|
|
235
|
+
return raw.split("\n").flatMap((line) => {
|
|
236
|
+
const sep = line.indexOf("\x1e");
|
|
237
|
+
if (sep < 0)
|
|
238
|
+
return [];
|
|
239
|
+
return [{
|
|
240
|
+
subject: line.slice(sep + 1),
|
|
241
|
+
merge: line.slice(0, sep).trim().includes(" "),
|
|
242
|
+
}];
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
export function createWorkerSubmissionGate(options = {}) {
|
|
246
|
+
let baseline;
|
|
247
|
+
let root;
|
|
248
|
+
let reminded = false;
|
|
249
|
+
let prefixReminded = false;
|
|
250
|
+
let unfinishedReasonBounces = 0;
|
|
251
|
+
let invocationScopeId;
|
|
252
|
+
/** Live parent — mid-turn report-ticket renames the run leaf and updates setSessionFile. */
|
|
253
|
+
let parentRef;
|
|
254
|
+
// #1132: one configured number, resolved once at gate construction.
|
|
255
|
+
const unfinishedReasonBounceLimit = deliveryLimitFromConfig(options.unfinishedReasonBounceLimit);
|
|
256
|
+
const head = (cwd) => {
|
|
257
|
+
try {
|
|
258
|
+
return git(cwd, ["rev-parse", "HEAD"]);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
git(cwd, ["rev-parse", "--git-dir"]); // surface real git failures
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
/** Current leg directory from the live parent session file. */
|
|
266
|
+
const liveDir = () => {
|
|
267
|
+
if (parentRef === undefined) {
|
|
268
|
+
throw new Error("Worker submission gate is not armed");
|
|
269
|
+
}
|
|
270
|
+
return liveRunDirectory(parentRef);
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
arm(cwd, parent, scope) {
|
|
274
|
+
invocationScopeId = scope;
|
|
275
|
+
uninstallPackageWorkerHooks(cwd);
|
|
276
|
+
root = cwd;
|
|
277
|
+
parentRef = parent;
|
|
278
|
+
const runDirectory = liveRunDirectory(parent);
|
|
279
|
+
const prior = readGateState(runDirectory, invocationScopeId);
|
|
280
|
+
unfinishedReasonBounces = prior.unfinishedReasonBounces;
|
|
281
|
+
if (prior.baseline !== undefined) {
|
|
282
|
+
baseline = prior.baseline;
|
|
283
|
+
reminded = prior.reminded;
|
|
284
|
+
prefixReminded = prior.prefixReminded;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
baseline = head(cwd);
|
|
288
|
+
reminded = false;
|
|
289
|
+
prefixReminded = false;
|
|
290
|
+
appendGateState(runDirectory, WORKER_COMMIT_BASELINE_ENTRY_TYPE, {
|
|
291
|
+
version: 1,
|
|
292
|
+
head: baseline,
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
assertAcceptable(status, details) {
|
|
296
|
+
if (status === "unfinished" && !unfinishedReasonPresent(details)) {
|
|
297
|
+
if (unfinishedReasonBounces < unfinishedReasonBounceLimit) {
|
|
298
|
+
if (parentRef !== undefined) {
|
|
299
|
+
appendGateState(liveDir(), WORKER_UNFINISHED_REASON_BOUNCE_ENTRY_TYPE, {
|
|
300
|
+
version: 1,
|
|
301
|
+
invocationScopeId,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
unfinishedReasonBounces += 1;
|
|
305
|
+
throw new WorkerUnfinishedReasonReminderError();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (baseline === undefined || root === undefined || !WORKER_DONE_STATUSES.has(status))
|
|
309
|
+
return;
|
|
310
|
+
const now = head(root);
|
|
311
|
+
const headMoved = now !== null && (baseline === null || now !== baseline);
|
|
312
|
+
// Gate ① — forgetfulness reminder (ADR 0066; behavior unchanged).
|
|
313
|
+
if (!headMoved && !reminded) {
|
|
314
|
+
reminded = true;
|
|
315
|
+
appendGateState(liveDir(), WORKER_COMMIT_REMINDER_BOUNCE_ENTRY_TYPE, { version: 1 });
|
|
316
|
+
throw new WorkerCommitReminderError();
|
|
317
|
+
}
|
|
318
|
+
reminded = true;
|
|
319
|
+
// Gate ② — open platform-prefix soft reminder (ADR 0070).
|
|
320
|
+
if (prefixReminded || now === null)
|
|
321
|
+
return;
|
|
322
|
+
const window = reliableWindow(root, baseline, now);
|
|
323
|
+
if (window === null ||
|
|
324
|
+
window.length === 0 ||
|
|
325
|
+
!window.some((c) => !c.merge && !PLATFORM_PREFIX.test(c.subject))) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
prefixReminded = true;
|
|
329
|
+
appendGateState(liveDir(), WORKER_PREFIX_REMINDER_BOUNCE_ENTRY_TYPE, { version: 1 });
|
|
330
|
+
throw new WorkerPrefixReminderError();
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
package/package.json
CHANGED
|
@@ -45,9 +45,9 @@ themselves. Stuffing large bodies into argv/prompt is the verified cause of
|
|
|
45
45
|
|
|
46
46
|
## Failure handling
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
connection-drop, or other engine-process failure,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
Prefer outsourcing labor to the selected engine rather than doing the work
|
|
49
|
+
in-seat (尽量外包,别自己干). On spawn, auth, quota, model-id, stream-stall,
|
|
50
|
+
connection-drop, or other engine-process failure, the real failure and any
|
|
51
|
+
process result come back to this same session; decide next steps here (retry,
|
|
52
|
+
report, or submit through the existing path). Do not silently swap to another
|
|
53
|
+
engine id.
|
package/resources/engines/agy.md
CHANGED
|
@@ -35,6 +35,6 @@ agy --sandbox --dangerously-skip-permissions --print 'YOUR_LABOR_PROMPT' --log-f
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
When the dispatch order names a model, pass it verbatim via `--model`; an
|
|
38
|
-
unknown model id is an engine-process failure (
|
|
38
|
+
unknown model id is an engine-process failure (returned to this seat — per
|
|
39
39
|
`../engine-dispatch.md`). Never copy a model id from this note or from a
|
|
40
40
|
previous run — ids here would go stale.
|
|
@@ -141,12 +141,16 @@ async function usageFromSummonedSession(summoned: PublicSummonResult): Promise<U
|
|
|
141
141
|
async function projectAuditorTerminal(summoned: PublicSummonResult): Promise<ComplianceDecision> {
|
|
142
142
|
const outcome = summoned.terminal?.roleOutcome;
|
|
143
143
|
if (outcome === undefined) {
|
|
144
|
+
// #1214 A5: officer process failure without a terminal stays transport_failure
|
|
145
|
+
// on the compliance decision — do not throw and kill the audited seat call.
|
|
144
146
|
const detail = summoned.stderr?.trim();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
return {
|
|
148
|
+
status: "transport_failure",
|
|
149
|
+
diagnostic: detail && detail.length > 0
|
|
147
150
|
? `Auditor public summon produced no terminal (exit ${summoned.exitCode}: ${detail})`
|
|
148
151
|
: `Auditor public summon produced no terminal (exit ${summoned.exitCode})`,
|
|
149
|
-
|
|
152
|
+
...(summoned.terminal === undefined ? {} : { terminal: summoned.terminal }),
|
|
153
|
+
};
|
|
150
154
|
}
|
|
151
155
|
const usage = await usageFromSummonedSession(summoned);
|
|
152
156
|
if (outcome.kind === "no_receipt") {
|