@akagilnc/pi-workflow-roles 0.1.1766 → 0.1.1772
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/package-contracts/fixer-output.js +1 -1
- package/dist/public-cli/main.js +9 -1
- package/package.json +1 -1
- package/souls/coder.md +0 -1
- package/souls/fixer.md +0 -1
- package/src/package-contracts/fixer-output.ts +1 -1
- package/src/package-contracts/worker-output.ts +1 -1
- package/src/public-cli/settlement.ts +8 -0
- package/src/worker-role.ts +16 -6
- package/src/worker-submission-gates.ts +50 -3
|
@@ -32,7 +32,7 @@ const completedClassResultsSchema = Type.Array(completedClassResultSchema, { min
|
|
|
32
32
|
const fixerOutputVariants = Type.Union([
|
|
33
33
|
Type.Object({ status: Type.Literal("planned", { description: "Plan-phase proposal outcome." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }) }),
|
|
34
34
|
Type.Object({ status: Type.Literal("refused", { description: "Lawfully refused outcome." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), remainingScope: Type.String({ minLength: 1, description: "Work that cannot lawfully be performed." }), blocker: Type.Unsafe({ ...blockerSchema, description: "Lawful blocker preventing completion." }) }),
|
|
35
|
-
Type.Object({ status: Type.Literal("unfinished", { description: "
|
|
35
|
+
Type.Object({ status: Type.Literal("unfinished", { description: "Apply outcome when a missing prerequisite or an unconstitutional constraint blocks completing this invocation; state the reason." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), remainingScope: Type.String({ minLength: 1, description: "Work remaining after this invocation." }), reason: Type.Optional(Type.String({ minLength: 1, description: "Blocking reason: prerequisite missing or unconstitutional. A missing pending owner decision or answer is a missing prerequisite." })), classResults: Type.Optional(Type.Unsafe({ ...completedClassResultsSchema, description: "Completed class settlements from this invocation." })), testEvidence: Type.Optional(testEvidenceSchema) }),
|
|
36
36
|
Type.Object({ status: Type.Literal("completed", { description: "All assigned classes completed." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), classResults: Type.Array(classResultSchema, { minItems: 1, description: "Completed class settlements." }), testEvidence: Type.Optional(testEvidenceSchema) }),
|
|
37
37
|
Type.Object({ status: Type.Literal("refused", { description: "All assigned classes lawfully refused." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), classResults: Type.Array(classResultSchema, { minItems: 1, description: "Per-class refusal settlements." }) }),
|
|
38
38
|
Type.Object({ status: Type.Literal("partially_completed", { description: "Assigned classes include completions and lawful refusals." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), classResults: Type.Array(classResultSchema, { minItems: 1, description: "Per-class completion or refusal settlements." }), testEvidence: Type.Optional(testEvidenceSchema) }),
|
package/dist/public-cli/main.js
CHANGED
|
@@ -13968,7 +13968,7 @@ var init_fixer_output = __esm({
|
|
|
13968
13968
|
fixerOutputVariants = typebox_exports.Union([
|
|
13969
13969
|
typebox_exports.Object({ status: typebox_exports.Literal("planned", { description: "Plan-phase proposal outcome." }), report: typebox_exports.String({ minLength: 1, description: "Truthful Fixer outcome report." }) }),
|
|
13970
13970
|
typebox_exports.Object({ status: typebox_exports.Literal("refused", { description: "Lawfully refused outcome." }), report: typebox_exports.String({ minLength: 1, description: "Truthful Fixer outcome report." }), remainingScope: typebox_exports.String({ minLength: 1, description: "Work that cannot lawfully be performed." }), blocker: typebox_exports.Unsafe({ ...blockerSchema, description: "Lawful blocker preventing completion." }) }),
|
|
13971
|
-
typebox_exports.Object({ status: typebox_exports.Literal("unfinished", { description: "
|
|
13971
|
+
typebox_exports.Object({ status: typebox_exports.Literal("unfinished", { description: "Apply outcome when a missing prerequisite or an unconstitutional constraint blocks completing this invocation; state the reason." }), report: typebox_exports.String({ minLength: 1, description: "Truthful Fixer outcome report." }), remainingScope: typebox_exports.String({ minLength: 1, description: "Work remaining after this invocation." }), reason: typebox_exports.Optional(typebox_exports.String({ minLength: 1, description: "Blocking reason: prerequisite missing or unconstitutional. A missing pending owner decision or answer is a missing prerequisite." })), classResults: typebox_exports.Optional(typebox_exports.Unsafe({ ...completedClassResultsSchema, description: "Completed class settlements from this invocation." })), testEvidence: typebox_exports.Optional(testEvidenceSchema) }),
|
|
13972
13972
|
typebox_exports.Object({ status: typebox_exports.Literal("completed", { description: "All assigned classes completed." }), report: typebox_exports.String({ minLength: 1, description: "Truthful Fixer outcome report." }), classResults: typebox_exports.Array(classResultSchema, { minItems: 1, description: "Completed class settlements." }), testEvidence: typebox_exports.Optional(testEvidenceSchema) }),
|
|
13973
13973
|
typebox_exports.Object({ status: typebox_exports.Literal("refused", { description: "All assigned classes lawfully refused." }), report: typebox_exports.String({ minLength: 1, description: "Truthful Fixer outcome report." }), classResults: typebox_exports.Array(classResultSchema, { minItems: 1, description: "Per-class refusal settlements." }) }),
|
|
13974
13974
|
typebox_exports.Object({ status: typebox_exports.Literal("partially_completed", { description: "Assigned classes include completions and lawful refusals." }), report: typebox_exports.String({ minLength: 1, description: "Truthful Fixer outcome report." }), classResults: typebox_exports.Array(classResultSchema, { minItems: 1, description: "Per-class completion or refusal settlements." }), testEvidence: typebox_exports.Optional(testEvidenceSchema) })
|
|
@@ -18777,6 +18777,10 @@ function coderDecisiveFacts(output) {
|
|
|
18777
18777
|
if (status.readable && typeof status.value === "string") facts.coderStatus = status.value;
|
|
18778
18778
|
const remainingScope = safelyRead(candidate, "remainingScope");
|
|
18779
18779
|
if (status.readable && status.value === "unfinished" && remainingScope.readable && typeof remainingScope.value === "string") facts.remainingScope = remainingScope.value;
|
|
18780
|
+
const reason = safelyRead(candidate, "reason");
|
|
18781
|
+
if (status.readable && status.value === "unfinished" && reason.readable && typeof reason.value === "string" && reason.value.trim().length > 0) {
|
|
18782
|
+
facts.reason = reason.value;
|
|
18783
|
+
}
|
|
18780
18784
|
const report = safelyRead(candidate, "report");
|
|
18781
18785
|
if (report.readable && typeof report.value === "string") facts.reportPresent = report.value.trim().length > 0;
|
|
18782
18786
|
return facts;
|
|
@@ -18788,6 +18792,10 @@ function fixerDecisiveFacts(output) {
|
|
|
18788
18792
|
if (status.readable && typeof status.value === "string") facts.fixerStatus = status.value;
|
|
18789
18793
|
const remainingScope = safelyRead(candidate, "remainingScope");
|
|
18790
18794
|
if (status.readable && (status.value === "unfinished" || status.value === "refused") && remainingScope.readable && typeof remainingScope.value === "string") facts.remainingScope = remainingScope.value;
|
|
18795
|
+
const reason = safelyRead(candidate, "reason");
|
|
18796
|
+
if (status.readable && status.value === "unfinished" && reason.readable && typeof reason.value === "string" && reason.value.trim().length > 0) {
|
|
18797
|
+
facts.reason = reason.value;
|
|
18798
|
+
}
|
|
18791
18799
|
const blockerRead = safelyRead(candidate, "blocker");
|
|
18792
18800
|
if (status.readable && status.value === "refused" && blockerRead.readable && isRecord4(blockerRead.value)) {
|
|
18793
18801
|
const cause = safelyRead(blockerRead.value, "cause");
|
package/package.json
CHANGED
package/souls/coder.md
CHANGED
|
@@ -6,6 +6,5 @@
|
|
|
6
6
|
- 做最小垂直切片,优先复用已有 seam;不造平行机制,不借机扩大 scope。
|
|
7
7
|
- 计划与施工都要说明真实刀口、行为边界和验证方法,不用空泛完成声明代替证据。
|
|
8
8
|
- 切片验证只跑本片聚焦测试与 typecheck。
|
|
9
|
-
- unfinished 仅一种情况可用:前置条件缺失、派单不合理或违宪导致本次无法完成,回执必须说明理由。
|
|
10
9
|
- 派单与 authority 冲突、事实不成立或需要未授权设计时,据理拒绝并给出可核验证据;不猜测施工。
|
|
11
10
|
- 不 amend、rewrite 或 push。Git 变化是证据之一,不取代角色报告。
|
package/souls/fixer.md
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* 前科检查:动刀前查相关文件 git log,不再提交既往已失败的同族修法。
|
|
9
9
|
* 测试是红线:不得放松断言、删失败路径或改验收换绿灯,除非 authority 明确授权。
|
|
10
10
|
* 每轮验证只跑与被修 finding 绑定的测试与 typecheck。
|
|
11
|
-
* unfinished 仅一种情况可用:前置条件缺失、派单不合理或违宪导致本次无法完成,回执必须说明理由。
|
|
12
11
|
* 只在当前指派无法合法执行时拒绝。
|
|
13
12
|
* 保护开工前已有改动;本轮只含授权内容,只建新 forward commit,不 amend。commit带平台前缀
|
|
14
13
|
* 交卷说明修了什么根因、未采纳的 finding 及理由,使修复与验证可独立审计。
|
|
@@ -35,7 +35,7 @@ const completedClassResultsSchema = Type.Array(completedClassResultSchema, { min
|
|
|
35
35
|
const fixerOutputVariants = Type.Union([
|
|
36
36
|
Type.Object({ status: Type.Literal("planned", { description: "Plan-phase proposal outcome." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }) }),
|
|
37
37
|
Type.Object({ status: Type.Literal("refused", { description: "Lawfully refused outcome." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), remainingScope: Type.String({ minLength: 1, description: "Work that cannot lawfully be performed." }), blocker: Type.Unsafe({ ...blockerSchema, description: "Lawful blocker preventing completion." }) }),
|
|
38
|
-
Type.Object({ status: Type.Literal("unfinished", { description: "
|
|
38
|
+
Type.Object({ status: Type.Literal("unfinished", { description: "Apply outcome when a missing prerequisite or an unconstitutional constraint blocks completing this invocation; state the reason." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), remainingScope: Type.String({ minLength: 1, description: "Work remaining after this invocation." }), reason: Type.Optional(Type.String({ minLength: 1, description: "Blocking reason: prerequisite missing or unconstitutional. A missing pending owner decision or answer is a missing prerequisite." })), classResults: Type.Optional(Type.Unsafe({ ...completedClassResultsSchema, description: "Completed class settlements from this invocation." })), testEvidence: Type.Optional(testEvidenceSchema) }),
|
|
39
39
|
Type.Object({ status: Type.Literal("completed", { description: "All assigned classes completed." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), classResults: Type.Array(classResultSchema, { minItems: 1, description: "Completed class settlements." }), testEvidence: Type.Optional(testEvidenceSchema) }),
|
|
40
40
|
Type.Object({ status: Type.Literal("refused", { description: "All assigned classes lawfully refused." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), classResults: Type.Array(classResultSchema, { minItems: 1, description: "Per-class refusal settlements." }) }),
|
|
41
41
|
Type.Object({ status: Type.Literal("partially_completed", { description: "Assigned classes include completions and lawful refusals." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), classResults: Type.Array(classResultSchema, { minItems: 1, description: "Per-class completion or refusal settlements." }), testEvidence: Type.Optional(testEvidenceSchema) }),
|
|
@@ -23,7 +23,7 @@ export type WorkerRoleLabel = "Coder" | "Fixer";
|
|
|
23
23
|
export type CoderOutput =
|
|
24
24
|
| { status: "planned"; report: string }
|
|
25
25
|
| { status: "completed" | "refused"; report: string }
|
|
26
|
-
| { status: "unfinished"; report: string; remainingScope: string };
|
|
26
|
+
| { status: "unfinished"; report: string; remainingScope: string; reason?: string };
|
|
27
27
|
export type WorkerOutput = CoderOutput | FixerOutput;
|
|
28
28
|
|
|
29
29
|
export function validateAcceptedCoderDetails(output: unknown): CoderOutput {
|
|
@@ -900,6 +900,10 @@ function coderDecisiveFacts(output: CoderOutput): Record<string, unknown> {
|
|
|
900
900
|
if (status.readable && typeof status.value === "string") facts.coderStatus = status.value;
|
|
901
901
|
const remainingScope = safelyRead(candidate, "remainingScope");
|
|
902
902
|
if (status.readable && status.value === "unfinished" && remainingScope.readable && typeof remainingScope.value === "string") facts.remainingScope = remainingScope.value;
|
|
903
|
+
const reason = safelyRead(candidate, "reason");
|
|
904
|
+
if (status.readable && status.value === "unfinished" && reason.readable && typeof reason.value === "string" && reason.value.trim().length > 0) {
|
|
905
|
+
facts.reason = reason.value;
|
|
906
|
+
}
|
|
903
907
|
const report = safelyRead(candidate, "report");
|
|
904
908
|
if (report.readable && typeof report.value === "string") facts.reportPresent = report.value.trim().length > 0;
|
|
905
909
|
return facts;
|
|
@@ -912,6 +916,10 @@ function fixerDecisiveFacts(output: FixerOutput): Record<string, unknown> {
|
|
|
912
916
|
if (status.readable && typeof status.value === "string") facts.fixerStatus = status.value;
|
|
913
917
|
const remainingScope = safelyRead(candidate, "remainingScope");
|
|
914
918
|
if (status.readable && (status.value === "unfinished" || status.value === "refused") && remainingScope.readable && typeof remainingScope.value === "string") facts.remainingScope = remainingScope.value;
|
|
919
|
+
const reason = safelyRead(candidate, "reason");
|
|
920
|
+
if (status.readable && status.value === "unfinished" && reason.readable && typeof reason.value === "string" && reason.value.trim().length > 0) {
|
|
921
|
+
facts.reason = reason.value;
|
|
922
|
+
}
|
|
915
923
|
const blockerRead = safelyRead(candidate, "blocker");
|
|
916
924
|
if (status.readable && status.value === "refused" && blockerRead.readable && isRecord(blockerRead.value)) {
|
|
917
925
|
const cause = safelyRead(blockerRead.value, "cause");
|
package/src/worker-role.ts
CHANGED
|
@@ -61,9 +61,17 @@ const coderOutputVariants = Type.Union([
|
|
|
61
61
|
report: Type.String({ minLength: 1, description: "Truthful Coder outcome report." }),
|
|
62
62
|
}, { additionalProperties: false }),
|
|
63
63
|
Type.Object({
|
|
64
|
-
status: StringEnum(["unfinished"] as const, {
|
|
64
|
+
status: StringEnum(["unfinished"] as const, {
|
|
65
|
+
description:
|
|
66
|
+
"Apply outcome when a missing prerequisite or an unconstitutional constraint blocks completing this invocation; state the reason.",
|
|
67
|
+
}),
|
|
65
68
|
report: Type.String({ minLength: 1, description: "Truthful Coder outcome report." }),
|
|
66
69
|
remainingScope: Type.String({ minLength: 1, description: "Work remaining after this invocation." }),
|
|
70
|
+
reason: Type.Optional(Type.String({
|
|
71
|
+
minLength: 1,
|
|
72
|
+
description:
|
|
73
|
+
"Blocking reason: prerequisite missing or unconstitutional. A missing pending owner decision or answer is a missing prerequisite.",
|
|
74
|
+
})),
|
|
67
75
|
}, { additionalProperties: false }),
|
|
68
76
|
]);
|
|
69
77
|
const coderOutputSchema = openToolObjectFromUnion(coderOutputVariants);
|
|
@@ -222,12 +230,13 @@ export function createFixerRoleRuntime(
|
|
|
222
230
|
name: FIXER_OUTPUT_TOOL_NAME,
|
|
223
231
|
label: "Fixer Output",
|
|
224
232
|
description:
|
|
225
|
-
"Submit the plan refusal, apply settlement, or
|
|
233
|
+
"Submit the plan refusal, apply settlement, or unfinished blocked-handover receipt.",
|
|
226
234
|
promptSnippet: "Submit the final fixer report",
|
|
227
235
|
promptGuidelines: [
|
|
228
236
|
`Use ${FIXER_OUTPUT_TOOL_NAME} as the final action for the fixer role.`,
|
|
229
237
|
`${FIXER_OUTPUT_TOOL_NAME} reports only lawful assignment blockers; infrastructure failures abort.`,
|
|
230
238
|
"plan permits planned|refused; apply permits completed|refused|partially_completed|unfinished.",
|
|
239
|
+
"unfinished is available only when a missing prerequisite or an unconstitutional constraint prevents completing this invocation; state the reason. A missing pending owner decision or answer is a missing prerequisite.",
|
|
231
240
|
"When the diff includes test changes, submit testEvidence: contract proven, one-line minimum necessary cost, measured duration.",
|
|
232
241
|
],
|
|
233
242
|
parameters: fixerOutputSchema,
|
|
@@ -242,7 +251,7 @@ export function createFixerRoleRuntime(
|
|
|
242
251
|
ctx,
|
|
243
252
|
);
|
|
244
253
|
const output = deepFreeze(validateFixerOutputForPacket(parameters, phase, packet));
|
|
245
|
-
submissionGate.assertAcceptable(output.status);
|
|
254
|
+
submissionGate.assertAcceptable(output.status, output);
|
|
246
255
|
return {
|
|
247
256
|
content: [{ type: "text" as const, text: "Fixer report accepted" }],
|
|
248
257
|
details: output,
|
|
@@ -345,12 +354,13 @@ export function createCoderRoleRuntime(
|
|
|
345
354
|
name: CODER_OUTPUT_TOOL_NAME,
|
|
346
355
|
label: "Coder Output",
|
|
347
356
|
description:
|
|
348
|
-
"Submit a plan, completion, unfinished
|
|
357
|
+
"Submit a plan, completion, unfinished blocked-handover, or evidence-bearing refusal for the active coder phase.",
|
|
349
358
|
promptSnippet: "Submit the final coder report",
|
|
350
359
|
promptGuidelines: [
|
|
351
360
|
`Use ${CODER_OUTPUT_TOOL_NAME} as the final action for the coder role.`,
|
|
352
361
|
`${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.
|
|
362
|
+
"plan permits planned|refused; apply permits completed|unfinished|refused.",
|
|
363
|
+
"unfinished is available only when a missing prerequisite or an unconstitutional constraint prevents completing this invocation; state the reason. A missing pending owner decision or answer is a missing prerequisite.",
|
|
354
364
|
"A completed apply report must preserve evidence for TDD, the same-pattern check, introduced-regression check, and behavior-fact check.",
|
|
355
365
|
],
|
|
356
366
|
parameters: coderOutputSchema,
|
|
@@ -373,7 +383,7 @@ export function createCoderRoleRuntime(
|
|
|
373
383
|
"Coder completed requires the Matt tdd skill to be expanded through Pi /skill:tdd",
|
|
374
384
|
);
|
|
375
385
|
}
|
|
376
|
-
submissionGate.assertAcceptable(output.status);
|
|
386
|
+
submissionGate.assertAcceptable(output.status, output);
|
|
377
387
|
return {
|
|
378
388
|
content: [{ type: "text" as const, text: "Coder report accepted" }],
|
|
379
389
|
details: output,
|
|
@@ -24,6 +24,27 @@ export class WorkerCommitReminderError extends Error {
|
|
|
24
24
|
constructor() { super("未观察到 commit"); this.name = "WorkerCommitReminderError"; }
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** #292 unfinished reason solicitation — same bounce shape as gate ①; in-session only. */
|
|
28
|
+
export class WorkerUnfinishedReasonReminderError extends Error {
|
|
29
|
+
readonly code = "worker_unfinished_reason_reminder" as const;
|
|
30
|
+
constructor() {
|
|
31
|
+
super("补理由(前置缺失/违宪之一)或继续施工");
|
|
32
|
+
this.name = "WorkerUnfinishedReasonReminderError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const UNFINISHED_REASON_BOUNCE_LIMIT = 2;
|
|
37
|
+
|
|
38
|
+
function unfinishedReasonPresent(details?: unknown): boolean {
|
|
39
|
+
if (typeof details !== "object" || details === null) return false;
|
|
40
|
+
try {
|
|
41
|
+
const reason = (details as { reason?: unknown }).reason;
|
|
42
|
+
return typeof reason === "string" && reason.trim().length > 0;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
export type WorkerSubmissionGateParent = RecordSessionParent;
|
|
28
49
|
|
|
29
50
|
function git(cwd: string, args: string[]): string {
|
|
@@ -90,7 +111,25 @@ export function installWorkerGitHooks(cwd: string): void {
|
|
|
90
111
|
try { bareInCommon = gitFile(commonConfig, ["--get", "core.bare"]) === "true"; } catch { /* unset */ }
|
|
91
112
|
try { worktreeInCommon = gitFile(commonConfig, ["--get", "core.worktree"]); } catch { /* unset */ }
|
|
92
113
|
|
|
93
|
-
|
|
114
|
+
// Skip shared write when already enabled in *this* repo — concurrent sibling activations
|
|
115
|
+
// otherwise race on .git/config.lock (#267). Scope must be --local (common config): a
|
|
116
|
+
// global/system true must not skip the repo's first enable. Value must use Git bool
|
|
117
|
+
// semantics (true/yes/on/1), not a literal "true" compare. First enable still writes;
|
|
118
|
+
// real write failures still throw. Only --get exit 1 means unset; other failures stay loud.
|
|
119
|
+
let worktreeConfigEnabled = false;
|
|
120
|
+
try {
|
|
121
|
+
worktreeConfigEnabled =
|
|
122
|
+
git(cwd, ["config", "--local", "--bool", "--get", "extensions.worktreeConfig"]) === "true";
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const status =
|
|
125
|
+
typeof error === "object" && error !== null && "status" in error
|
|
126
|
+
? (error as { status: unknown }).status
|
|
127
|
+
: undefined;
|
|
128
|
+
if (status !== 1) throw error;
|
|
129
|
+
}
|
|
130
|
+
if (!worktreeConfigEnabled) {
|
|
131
|
+
git(cwd, ["config", "extensions.worktreeConfig", "true"]);
|
|
132
|
+
}
|
|
94
133
|
|
|
95
134
|
// Migrate immediately so sibling trees never observe bare-in-common under worktreeConfig.
|
|
96
135
|
if (bareInCommon) {
|
|
@@ -176,11 +215,12 @@ function readGateState(session: SessionManager): {
|
|
|
176
215
|
|
|
177
216
|
export function createWorkerSubmissionGate(): {
|
|
178
217
|
arm(cwd: string, parent?: WorkerSubmissionGateParent): void;
|
|
179
|
-
assertAcceptable(status: string): void;
|
|
218
|
+
assertAcceptable(status: string, details?: unknown): void;
|
|
180
219
|
} {
|
|
181
220
|
let baseline: string | null | undefined;
|
|
182
221
|
let root: string | undefined;
|
|
183
222
|
let reminded = false;
|
|
223
|
+
let unfinishedReasonBounces = 0;
|
|
184
224
|
let record: SessionManager | undefined;
|
|
185
225
|
// null = unborn HEAD only; any other git failure throws (no swallow).
|
|
186
226
|
const head = (cwd: string): string | null => {
|
|
@@ -209,7 +249,14 @@ export function createWorkerSubmissionGate(): {
|
|
|
209
249
|
head: baseline,
|
|
210
250
|
});
|
|
211
251
|
},
|
|
212
|
-
assertAcceptable(status) {
|
|
252
|
+
assertAcceptable(status, details) {
|
|
253
|
+
// #292: unfinished without a non-blank reason → in-session bounce (max 2), then accept.
|
|
254
|
+
if (status === "unfinished" && !unfinishedReasonPresent(details)) {
|
|
255
|
+
if (unfinishedReasonBounces < UNFINISHED_REASON_BOUNCE_LIMIT) {
|
|
256
|
+
unfinishedReasonBounces += 1;
|
|
257
|
+
throw new WorkerUnfinishedReasonReminderError();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
213
260
|
if (baseline === undefined || root === undefined || !DONE.has(status)) return;
|
|
214
261
|
const now = head(root);
|
|
215
262
|
if ((now !== null && (baseline === null || now !== baseline)) || reminded) {
|