@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.
@@ -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: "Honest unfinished apply outcome." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), remainingScope: Type.String({ minLength: 1, description: "Work remaining after this invocation." }), classResults: Type.Optional(Type.Unsafe({ ...completedClassResultsSchema, description: "Completed class settlements from this invocation." })), testEvidence: Type.Optional(testEvidenceSchema) }),
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) }),
@@ -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: "Honest unfinished apply outcome." }), report: typebox_exports.String({ minLength: 1, description: "Truthful Fixer outcome report." }), remainingScope: typebox_exports.String({ minLength: 1, description: "Work remaining after this invocation." }), classResults: typebox_exports.Optional(typebox_exports.Unsafe({ ...completedClassResultsSchema, description: "Completed class settlements from this invocation." })), testEvidence: typebox_exports.Optional(testEvidenceSchema) }),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.1766",
3
+ "version": "0.1.1772",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
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: "Honest unfinished apply outcome." }), report: Type.String({ minLength: 1, description: "Truthful Fixer outcome report." }), remainingScope: Type.String({ minLength: 1, description: "Work remaining after this invocation." }), classResults: Type.Optional(Type.Unsafe({ ...completedClassResultsSchema, description: "Completed class settlements from this invocation." })), testEvidence: Type.Optional(testEvidenceSchema) }),
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");
@@ -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, { description: "Honest unfinished apply outcome." }),
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 honest unfinished handover.",
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 handoff, or evidence-bearing refusal for the active coder phase.",
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. unfinished requires a non-blank remainingScope.",
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
- git(cwd, ["config", "extensions.worktreeConfig", "true"]);
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) {