project-tiny-context-harness 0.2.83 → 0.2.85

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.
Files changed (31) hide show
  1. package/README.md +5 -5
  2. package/assets/README.md +8 -8
  3. package/assets/README.zh-CN.md +4 -4
  4. package/assets/protected-harness-baseline.json +6 -4
  5. package/assets/skills/composite-long-task-workflow/SKILL.md +12 -5
  6. package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +19 -0
  7. package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +2 -2
  8. package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +47 -26
  9. package/dist/commands/composite-long-task.js +21 -3
  10. package/dist/lib/package-source.js +11 -1
  11. package/dist/lib/superpowers-task-command-run-correlation.d.ts +8 -0
  12. package/dist/lib/superpowers-task-command-run-correlation.js +103 -0
  13. package/dist/lib/superpowers-task-completion-output.d.ts +91 -0
  14. package/dist/lib/superpowers-task-completion-output.js +374 -0
  15. package/dist/lib/superpowers-task-current-evidence.js +22 -0
  16. package/dist/lib/superpowers-task-derive.js +66 -8
  17. package/dist/lib/superpowers-task-evidence-kernel.d.ts +7 -0
  18. package/dist/lib/superpowers-task-evidence-kernel.js +67 -71
  19. package/dist/lib/superpowers-task-evidence.js +7 -1
  20. package/dist/lib/superpowers-task-final-card.d.ts +3 -0
  21. package/dist/lib/superpowers-task-final-card.js +35 -0
  22. package/dist/lib/superpowers-task-gates.d.ts +2 -2
  23. package/dist/lib/superpowers-task-gates.js +179 -27
  24. package/dist/lib/superpowers-task-harness-drift.js +6 -2
  25. package/dist/lib/superpowers-task-protected-baseline.js +21 -2
  26. package/dist/lib/superpowers-task-state-schema.d.ts +37 -0
  27. package/dist/lib/superpowers-task-state.js +10 -1
  28. package/dist/lib/superpowers-task-unregistered-evidence.d.ts +11 -0
  29. package/dist/lib/superpowers-task-unregistered-evidence.js +72 -0
  30. package/dist/lib/superpowers-task-validator.js +41 -0
  31. package/package.json +69 -69
@@ -2,7 +2,9 @@ import { appendSuperpowersEvent } from "./superpowers-task-events.js";
2
2
  import { deriveSuperpowersArtifacts } from "./superpowers-task-derive.js";
3
3
  import { validatePlanAcceptance } from "./plan-acceptance-validator.js";
4
4
  import { loadSuperpowersState, saveSuperpowersState } from "./superpowers-task-state.js";
5
+ import { applyCompletionOutputContract, completionPhraseFindingMessages, resolveCompletionOutputStatus, scanGeneratedCompletionOutputSurfacesDetailed, triageFinalGateBlockers } from "./superpowers-task-completion-output.js";
5
6
  import { applyTrustedEvidenceKernelResult, evaluateTrustedEvidenceKernel } from "./superpowers-task-evidence-kernel.js";
7
+ import { isRecord } from "./superpowers-task-state-schema.js";
6
8
  import { validateSuperpowersState } from "./superpowers-task-validator.js";
7
9
  export async function runSliceGate(workdir, sliceId) {
8
10
  const state = await loadSuperpowersState(workdir);
@@ -24,25 +26,48 @@ export async function runEpochGate(workdir, epochId) {
24
26
  return { passed: true, messages: ["epoch derived artifacts refreshed"] };
25
27
  }
26
28
  export async function runFinalGate(workdir) {
29
+ return runFinalGateOnce(workdir, { recoveryAttempted: false, recoveryAction: "" });
30
+ }
31
+ async function runFinalGateOnce(workdir, options) {
27
32
  const state = await loadSuperpowersState(workdir);
33
+ const previousTransientFindings = previousTransientBookkeepingFindings(state);
28
34
  const kernel = await evaluateTrustedEvidenceKernel(workdir, state);
29
35
  applyTrustedEvidenceKernelResult(state, kernel);
30
36
  state.gates.validator = { status: "not_run", kernel: "trusted_evidence_kernel" };
37
+ const candidateContract = resolveCompletionOutputStatus({
38
+ final_gate_ran: true,
39
+ product_goal_complete: kernel.product_goal_complete && kernel.errors.length === 0,
40
+ acceptance_target_status: kernel.product_goal_complete && kernel.errors.length === 0 ? "complete" : kernel.acceptance_target_status,
41
+ audit_task_complete: true,
42
+ validator_errors: kernel.errors,
43
+ rejection_reasons: kernel.product_goal_complete ? [] : kernel.errors.slice(0, 12)
44
+ });
45
+ const candidateState = candidateStateFromContract(candidateContract);
46
+ candidateContract.candidate_state = candidateState;
47
+ applyCompletionOutputContract(state, candidateContract);
48
+ state.final.completion_basis = candidateContract.product_goal_complete
49
+ ? ["trusted_evidence_kernel", "current_attempt_evidence", "negative_evidence_scan_passed", "harness_drift_lock_passed"]
50
+ : [];
31
51
  await saveSuperpowersState(workdir, state);
32
52
  await deriveSuperpowersArtifacts(workdir);
33
53
  const report = await validateSuperpowersState(workdir, [workdir]);
34
54
  const acceptanceReport = await validatePlanAcceptance(workdir, [workdir]);
35
55
  const latest = await loadSuperpowersState(workdir);
36
- const errors = [...new Set([...kernel.errors, ...report.errors, ...acceptanceReport.errors])];
37
- const complete = kernel.product_goal_complete && errors.length === 0;
56
+ let errors = [...new Set([...kernel.errors, ...report.errors, ...acceptanceReport.errors])];
57
+ let complete = kernel.product_goal_complete && errors.length === 0;
38
58
  const acceptanceStatus = complete ? "complete" : acceptanceStatusForErrors(errors, kernel);
39
59
  const nextRequiredActions = complete ? [] : nextActionsForErrors(errors);
40
- latest.final.product_goal_complete = complete;
41
- latest.meta.product_goal_complete = complete;
42
- latest.final.acceptance_target_status = acceptanceStatus;
43
- latest.meta.acceptance_target_status = latest.final.acceptance_target_status;
44
- latest.final.audit_task_complete = true;
45
- latest.meta.audit_task_complete = true;
60
+ let contract = resolveCompletionOutputStatus({
61
+ final_gate_ran: true,
62
+ product_goal_complete: complete,
63
+ acceptance_target_status: acceptanceStatus,
64
+ audit_task_complete: true,
65
+ validator_errors: report.errors,
66
+ acceptance_validator_errors: acceptanceReport.errors,
67
+ rejection_reasons: complete ? [] : nextRequiredActions
68
+ });
69
+ contract.candidate_state = candidateState;
70
+ applyCompletionOutputContract(latest, contract);
46
71
  latest.final.completion_basis = complete
47
72
  ? ["trusted_evidence_kernel", "current_attempt_evidence", "negative_evidence_scan_passed", "harness_drift_lock_passed"]
48
73
  : [];
@@ -51,31 +76,126 @@ export async function runFinalGate(workdir) {
51
76
  latest.gates.final_gate = {
52
77
  status: complete ? "pass" : acceptanceStatus,
53
78
  kernel: "trusted_evidence_kernel",
54
- order: [
55
- "load_three_inputs",
56
- "recompute_source_hashes",
57
- "load_task_state",
58
- "load_current_attempt",
59
- "load_command_run_records",
60
- "load_registered_evidence_records",
61
- "discard_stale_evidence",
62
- "contradiction_scan",
63
- "recompute_every_ac",
64
- "recompute_every_pi",
65
- "recompute_acceptance_target_status",
66
- "recompute_product_goal_complete",
67
- "regenerate_derived",
68
- "append_event"
69
- ],
79
+ order: kernel.kernel_order,
70
80
  errors,
71
81
  stale_evidence_ids: kernel.stale_evidence_ids,
82
+ ignored_unregistered_evidence: kernel.ignored_unregistered_evidence,
83
+ invalidated_evidence_ids: kernel.invalidated_evidence_ids,
84
+ ac_findings: kernel.ac_findings,
85
+ pi_findings: kernel.pi_findings,
72
86
  harness_task_final_verdict: kernel.harness_task_final_verdict,
73
- next_required_actions: nextRequiredActions
87
+ next_required_actions: nextRequiredActions,
88
+ completion_output_status: contract.completion_output_status,
89
+ final_answer_allowed: contract.final_answer_allowed,
90
+ required_user_visible_status: contract.required_user_visible_status,
91
+ exit_code: contract.exit_code,
92
+ blocked_reasons: contract.blocked_reasons,
93
+ rejection_reasons: contract.rejection_reasons,
94
+ generated_output_mismatch: contract.generated_output_mismatch,
95
+ candidate_state: candidateState,
96
+ previous_bookkeeping_snapshot: previousTransientFindings
74
97
  };
75
98
  await saveSuperpowersState(workdir, latest);
76
99
  await deriveSuperpowersArtifacts(workdir);
77
- await appendSuperpowersEvent(workdir, "final_gate", { product_goal_complete: complete });
78
- return { product_goal_complete: complete, errors };
100
+ const outputFindings = await scanGeneratedCompletionOutputSurfacesDetailed(workdir, contract);
101
+ let triage = triageFinalGateBlockers({
102
+ errors,
103
+ output_findings: outputFindings,
104
+ previous_transient_findings: previousTransientFindings,
105
+ candidate_state: candidateState,
106
+ recovery_attempted: options.recoveryAttempted || previousTransientFindings.length > 0,
107
+ recovery_action: options.recoveryAction || (previousTransientFindings.length > 0 ? "cleared previous transient bookkeeping" : "")
108
+ });
109
+ if (outputFindings.length > 0 && triage.self_recoverable && !options.recoveryAttempted) {
110
+ await deriveSuperpowersArtifacts(workdir);
111
+ return runFinalGateOnce(workdir, { recoveryAttempted: true, recoveryAction: "regenerated_derived_outputs" });
112
+ }
113
+ if (outputFindings.length > 0) {
114
+ errors = [...new Set([...errors, ...completionPhraseFindingMessages(outputFindings)])];
115
+ complete = false;
116
+ triage = triageFinalGateBlockers({
117
+ errors,
118
+ output_findings: outputFindings,
119
+ previous_transient_findings: previousTransientFindings,
120
+ candidate_state: candidateState,
121
+ recovery_attempted: options.recoveryAttempted,
122
+ recovery_action: options.recoveryAction
123
+ });
124
+ contract = resolveCompletionOutputStatus({
125
+ final_gate_ran: true,
126
+ product_goal_complete: false,
127
+ acceptance_target_status: acceptanceStatusForErrors(errors, kernel),
128
+ audit_task_complete: true,
129
+ validator_errors: errors,
130
+ generated_output_mismatch: true
131
+ });
132
+ contract.candidate_state = candidateState;
133
+ contract.blocker_triage = triage;
134
+ const blockedLatest = await loadSuperpowersState(workdir);
135
+ applyCompletionOutputContract(blockedLatest, contract);
136
+ blockedLatest.final.completion_basis = [];
137
+ blockedLatest.final.next_required_actions = nextActionsForErrors(errors);
138
+ blockedLatest.gates.validator = { status: "blocked", errors };
139
+ blockedLatest.gates.final_gate = {
140
+ ...(isRecord(blockedLatest.gates.final_gate) ? blockedLatest.gates.final_gate : {}),
141
+ status: contract.completion_output_status,
142
+ errors,
143
+ next_required_actions: blockedLatest.final.next_required_actions,
144
+ completion_output_status: contract.completion_output_status,
145
+ final_answer_allowed: contract.final_answer_allowed,
146
+ required_user_visible_status: contract.required_user_visible_status,
147
+ exit_code: contract.exit_code,
148
+ blocked_reasons: contract.blocked_reasons,
149
+ rejection_reasons: contract.rejection_reasons,
150
+ generated_output_mismatch: contract.generated_output_mismatch,
151
+ false_completion_phrase_findings: outputFindings,
152
+ candidate_state: candidateState,
153
+ blocker_triage: triage,
154
+ previous_bookkeeping_snapshot: previousTransientFindings
155
+ };
156
+ blockedLatest.final.false_completion_phrase_findings = outputFindings;
157
+ await saveSuperpowersState(workdir, blockedLatest);
158
+ await deriveSuperpowersArtifacts(workdir);
159
+ }
160
+ else {
161
+ if (triage.category === "environment_blocked" || triage.category === "contract_blocked" || triage.category === "harness_drift_blocked") {
162
+ contract = resolveCompletionOutputStatus({
163
+ final_gate_ran: true,
164
+ product_goal_complete: false,
165
+ acceptance_target_status: "blocked",
166
+ audit_task_complete: true,
167
+ validator_errors: errors,
168
+ blocked_reasons: [triage.category],
169
+ rejection_reasons: []
170
+ });
171
+ contract.candidate_state = candidateState;
172
+ }
173
+ contract.blocker_triage = triage;
174
+ const triagedLatest = await loadSuperpowersState(workdir);
175
+ applyCompletionOutputContract(triagedLatest, contract);
176
+ triagedLatest.final.next_required_actions = contract.completion_output_status === "accept" ? [] : nextActionsForErrors(errors);
177
+ triagedLatest.gates.final_gate = {
178
+ ...(isRecord(triagedLatest.gates.final_gate) ? triagedLatest.gates.final_gate : {}),
179
+ completion_output_status: contract.completion_output_status,
180
+ final_answer_allowed: contract.final_answer_allowed,
181
+ required_user_visible_status: contract.required_user_visible_status,
182
+ exit_code: contract.exit_code,
183
+ blocked_reasons: contract.blocked_reasons,
184
+ rejection_reasons: contract.rejection_reasons,
185
+ generated_output_mismatch: contract.generated_output_mismatch,
186
+ candidate_state: candidateState,
187
+ blocker_triage: triage,
188
+ previous_bookkeeping_snapshot: previousTransientFindings
189
+ };
190
+ await saveSuperpowersState(workdir, triagedLatest);
191
+ await deriveSuperpowersArtifacts(workdir);
192
+ }
193
+ await appendSuperpowersEvent(workdir, "final_gate", {
194
+ product_goal_complete: contract.product_goal_complete,
195
+ completion_output_status: contract.completion_output_status,
196
+ blocker_triage: contract.blocker_triage
197
+ });
198
+ return { ...contract, blocker_triage: contract.blocker_triage ?? triage, errors };
79
199
  }
80
200
  function acceptanceStatusForErrors(errors, kernel) {
81
201
  const text = errors.join("\n");
@@ -113,3 +233,35 @@ function nextActionsForErrors(errors) {
113
233
  return error;
114
234
  });
115
235
  }
236
+ function candidateStateFromContract(contract) {
237
+ return {
238
+ final_gate_ran: contract.final_gate_ran,
239
+ product_goal_complete: contract.product_goal_complete,
240
+ acceptance_target_status: contract.acceptance_target_status,
241
+ completion_output_status: contract.completion_output_status,
242
+ generated_output_mismatch: false,
243
+ source: "trusted_evidence_kernel"
244
+ };
245
+ }
246
+ function previousTransientBookkeepingFindings(state) {
247
+ const findings = [];
248
+ const finalRecord = state.final;
249
+ const metaRecord = state.meta;
250
+ const gate = isRecord(state.gates?.final_gate) ? state.gates.final_gate : {};
251
+ collectTransient(findings, "final", finalRecord);
252
+ collectTransient(findings, "meta", metaRecord);
253
+ collectTransient(findings, "gates.final_gate", gate);
254
+ return [...new Set(findings)];
255
+ }
256
+ function collectTransient(findings, label, record) {
257
+ const status = typeof record.completion_output_status === "string" ? record.completion_output_status : "";
258
+ if (status === "blocked" || status === "reject") {
259
+ findings.push(`${label}.completion_output_status=${status}`);
260
+ }
261
+ if (record.generated_output_mismatch === true) {
262
+ findings.push(`${label}.generated_output_mismatch=true`);
263
+ }
264
+ if (Array.isArray(record.false_completion_phrase_findings) && record.false_completion_phrase_findings.length > 0) {
265
+ findings.push(`${label}.false_completion_phrase_findings=${record.false_completion_phrase_findings.length}`);
266
+ }
267
+ }
@@ -4,7 +4,8 @@ const HARNESS_PATH_PATTERNS = [
4
4
  /(^|\/)playwright\.config\./i,
5
5
  /(^|\/).*\.spec\.[cm]?[jt]sx?$/i,
6
6
  /(^|\/).*\.test\.[cm]?[jt]sx?$/i,
7
- /superpowers-task-(assertions|evidence|evidence-kernel|current-evidence|command-specs|ac010|gates|validator|derive|state|state-schema|state-shape|under-specified|protected-baseline|harness-drift)\.ts$/i,
7
+ /(^|\/)tests\/ty-context\/fixtures\/composite-long-task\//i,
8
+ /superpowers-task-(assertions|evidence|evidence-kernel|current-evidence|command-specs|command-run-correlation|unregistered-evidence|completion-output|final-card|ac010|gates|validator|derive|state|state-schema|state-shape|under-specified|protected-baseline|harness-drift)\.ts$/i,
8
9
  /(^|\/)(\.codex\/ty-context-managed|packages\/ty-context\/assets)\/skills\/composite-long-task-workflow\//i,
9
10
  /composite-long-task-workflow-protocol\.md$/i,
10
11
  /(^|\/)Makefile$/i,
@@ -79,7 +80,10 @@ function harnessTaskFixtureVerdict(state, changedFiles) {
79
80
  };
80
81
  }
81
82
  function currentAttempt(state) {
82
- return (state.attempts ?? []).find((item) => item.task_attempt_id === state.current_attempt_id) ?? (state.attempts ?? []).at(-1);
83
+ if (!state.current_attempt_id) {
84
+ return undefined;
85
+ }
86
+ return (state.attempts ?? []).find((item) => item.task_attempt_id === state.current_attempt_id);
83
87
  }
84
88
  function changedFilesFor(attempt) {
85
89
  return [...new Set((attempt?.changed_files ?? []).map((file) => file.replace(/\\/g, "/")).filter(Boolean))];
@@ -5,15 +5,31 @@ export const PROTECTED_BASELINE_PATHS = [
5
5
  ".codex/ty-context-managed/protected-harness-baseline.json",
6
6
  "packages/ty-context/source-mappings.yaml",
7
7
  "packages/ty-context/src/lib/superpowers-task-gates.ts",
8
+ "packages/ty-context/src/lib/superpowers-task-completion-output.ts",
9
+ "packages/ty-context/src/lib/superpowers-task-final-card.ts",
8
10
  "packages/ty-context/src/lib/superpowers-task-validator.ts",
9
11
  "packages/ty-context/src/lib/superpowers-task-derive.ts",
10
12
  "packages/ty-context/src/lib/superpowers-task-evidence.ts",
11
13
  "packages/ty-context/src/lib/superpowers-task-evidence-kernel.ts",
14
+ "packages/ty-context/src/lib/superpowers-task-current-evidence.ts",
15
+ "packages/ty-context/src/lib/superpowers-task-command-run-correlation.ts",
16
+ "packages/ty-context/src/lib/superpowers-task-unregistered-evidence.ts",
17
+ "packages/ty-context/src/lib/superpowers-task-ac010.ts",
18
+ "packages/ty-context/src/lib/superpowers-task-harness-drift.ts",
19
+ "packages/ty-context/src/lib/superpowers-task-protected-baseline.ts",
12
20
  "packages/ty-context/src/lib/superpowers-task-state-schema.ts",
13
21
  ".codex/ty-context-managed/skills/composite-long-task-workflow/SKILL.md",
14
22
  ".codex/ty-context-managed/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md",
23
+ ".codex/ty-context-managed/skills/composite-long-task-workflow/assets/goal-objective.template.md",
24
+ ".codex/ty-context-managed/skills/composite-long-task-workflow/assets/execution-binding.template.md",
15
25
  "packages/ty-context/assets/skills/composite-long-task-workflow/SKILL.md",
16
- "packages/ty-context/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md"
26
+ "packages/ty-context/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md",
27
+ "packages/ty-context/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md",
28
+ "packages/ty-context/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md",
29
+ "tests/ty-context/composite-long-task-completion-output-gate.test.mjs",
30
+ "tests/ty-context/fixtures/composite-long-task/completion-output-gate/expected-outcomes.json",
31
+ "tests/ty-context/composite-long-task-false-completion-regression.test.mjs",
32
+ "tests/ty-context/fixtures/composite-long-task/false-completion-regression/manifest.json"
17
33
  ];
18
34
  export function evaluateProtectedBaseline(state) {
19
35
  const attempt = currentAttempt(state);
@@ -43,5 +59,8 @@ function protectedBaselineReason(state) {
43
59
  return isRecord(baseline) && typeof baseline.reason === "string" && baseline.reason.trim().length > 0;
44
60
  }
45
61
  function currentAttempt(state) {
46
- return (state.attempts ?? []).find((item) => item.task_attempt_id === state.current_attempt_id) ?? (state.attempts ?? []).at(-1);
62
+ if (!state.current_attempt_id) {
63
+ return undefined;
64
+ }
65
+ return (state.attempts ?? []).find((item) => item.task_attempt_id === state.current_attempt_id);
47
66
  }
@@ -76,6 +76,25 @@ export type SuperpowersProductDeliveryScope = "system_capability_build" | "repre
76
76
  export type SuperpowersPlanDeliveryScope = "system_capability_build" | "representative_sample_validation" | "full_population_operation" | "out_of_scope_backlog";
77
77
  export type SuperpowersAcceptanceScope = "system_capability_build" | "representative_sample_validation" | "full_population_operation" | "full_population_not_required";
78
78
  export type SuperpowersScopeFitDecision = "fit_for_three_inputs" | "selected_from_split" | "blocked_for_decision" | "";
79
+ export type CompletionOutputStatus = "accept" | "reject" | "blocked";
80
+ export type FinalGateBlockerCategory = "none" | "product_evidence_failed" | "missing_current_evidence" | "stale_or_contradictory_evidence" | "generated_output_mismatch" | "self_recoverable_generated_output_mismatch" | "transient_state_bookkeeping" | "environment_blocked" | "contract_blocked" | "harness_drift_blocked";
81
+ export interface FinalGateCandidateStateRecord {
82
+ final_gate_ran: boolean;
83
+ product_goal_complete: boolean;
84
+ acceptance_target_status: string;
85
+ completion_output_status: CompletionOutputStatus;
86
+ generated_output_mismatch: boolean;
87
+ source: string;
88
+ }
89
+ export interface FinalGateBlockerTriageRecord {
90
+ category: FinalGateBlockerCategory;
91
+ self_recoverable: boolean;
92
+ recovery_attempted: boolean;
93
+ recovery_action: string;
94
+ next_action: string;
95
+ details: string[];
96
+ blocker_count: number;
97
+ }
79
98
  export interface SuperpowersTaskState {
80
99
  meta: {
81
100
  task_id: string;
@@ -87,6 +106,7 @@ export interface SuperpowersTaskState {
87
106
  product_goal_complete: boolean;
88
107
  acceptance_target_status: string;
89
108
  audit_task_complete: boolean;
109
+ completion_output_status?: CompletionOutputStatus;
90
110
  };
91
111
  sources: Record<string, SuperpowersSourceRecord>;
92
112
  context: {
@@ -116,6 +136,17 @@ export interface SuperpowersTaskState {
116
136
  product_goal_complete: boolean;
117
137
  acceptance_target_status: string;
118
138
  audit_task_complete: boolean;
139
+ completion_output_status?: CompletionOutputStatus;
140
+ final_answer_allowed?: boolean;
141
+ required_user_visible_status?: "accepted" | "rejected" | "blocked";
142
+ final_answer?: CompletionOutputStatus;
143
+ exit_code?: 0 | 1 | 2;
144
+ blocked_reasons?: string[];
145
+ rejection_reasons?: string[];
146
+ generated_output_mismatch?: boolean;
147
+ false_completion_phrase_findings?: unknown[];
148
+ blocker_triage?: FinalGateBlockerTriageRecord;
149
+ candidate_state?: FinalGateCandidateStateRecord;
119
150
  completion_basis: string[];
120
151
  next_required_actions?: string[];
121
152
  };
@@ -162,6 +193,7 @@ export interface CommandRunRecord {
162
193
  command_line: string;
163
194
  exit_code: number;
164
195
  started_at: string;
196
+ completed_at?: string;
165
197
  ended_at: string;
166
198
  artifact_paths: string[];
167
199
  }
@@ -336,11 +368,16 @@ export interface SuperpowersEvidenceRecord {
336
368
  schema_version?: "evidence-record-v1" | "evidence-record-v2" | string;
337
369
  evidence_id: string;
338
370
  task_attempt_id?: string;
371
+ generated_at?: string;
339
372
  source_bundle_hash?: string;
340
373
  product_source_hash?: string;
341
374
  technical_plan_hash?: string;
342
375
  acceptance_checklist_hash?: string;
343
376
  git_head?: string;
377
+ git_status_short?: string;
378
+ tracked_diff_hash?: string;
379
+ relevant_untracked_hash?: string;
380
+ covers_dirty_worktree?: boolean;
344
381
  worktree_fingerprint?: string;
345
382
  command_spec_id?: string;
346
383
  command_run_id?: string;
@@ -41,7 +41,8 @@ export async function initializeSuperpowersTask(workdir, options = {}) {
41
41
  goal_type: options.goalType ?? "implementation",
42
42
  product_goal_complete: false,
43
43
  acceptance_target_status: "not_run",
44
- audit_task_complete: false
44
+ audit_task_complete: false,
45
+ completion_output_status: "blocked"
45
46
  },
46
47
  sources: await sourceRecords(workdir),
47
48
  context: {
@@ -88,6 +89,14 @@ export async function initializeSuperpowersTask(workdir, options = {}) {
88
89
  product_goal_complete: false,
89
90
  acceptance_target_status: "not_run",
90
91
  audit_task_complete: false,
92
+ completion_output_status: "blocked",
93
+ final_answer_allowed: false,
94
+ required_user_visible_status: "blocked",
95
+ final_answer: "blocked",
96
+ exit_code: 2,
97
+ blocked_reasons: ["final_gate_not_run"],
98
+ rejection_reasons: [],
99
+ generated_output_mismatch: false,
91
100
  completion_basis: [],
92
101
  next_required_actions: []
93
102
  }
@@ -0,0 +1,11 @@
1
+ import { type SuperpowersTaskState } from "./superpowers-task-state-schema.js";
2
+ export interface IgnoredUnregisteredEvidence {
3
+ path: string;
4
+ status: string;
5
+ target_ac_ids: string[];
6
+ target_proof_layers: string[];
7
+ }
8
+ export declare function scanUnregisteredAssertionEvidence(workdir: string, state: SuperpowersTaskState): Promise<{
9
+ ignored: IgnoredUnregisteredEvidence[];
10
+ errors: string[];
11
+ }>;
@@ -0,0 +1,72 @@
1
+ import path from "node:path";
2
+ import { listFiles, readText } from "./fs.js";
3
+ import { isRecord } from "./superpowers-task-state-schema.js";
4
+ export async function scanUnregisteredAssertionEvidence(workdir, state) {
5
+ const registeredPaths = new Set();
6
+ for (const evidence of state.evidence ?? []) {
7
+ for (const candidate of [evidence.artifact_path, ...(evidence.artifact_paths ?? []), ...(evidence.assertion_result?.artifacts ?? [])]) {
8
+ if (candidate) {
9
+ registeredPaths.add(slash(candidate));
10
+ registeredPaths.add(slash(path.join(workdir, candidate)));
11
+ }
12
+ }
13
+ }
14
+ const ignored = [];
15
+ for (const file of await listFiles(workdir)) {
16
+ if (!/\.json$/i.test(file)) {
17
+ continue;
18
+ }
19
+ const relative = slash(path.relative(workdir, file));
20
+ if (registeredPaths.has(relative) || registeredPaths.has(slash(file))) {
21
+ continue;
22
+ }
23
+ const parsed = await readJson(file);
24
+ const assertion = assertionRecord(parsed);
25
+ if (!assertion) {
26
+ continue;
27
+ }
28
+ const status = String(assertion.status ?? "");
29
+ if (status !== "passed") {
30
+ continue;
31
+ }
32
+ ignored.push({
33
+ path: relative,
34
+ status,
35
+ target_ac_ids: asStringArray(assertion.target_ac_ids),
36
+ target_proof_layers: asStringArray(assertion.target_proof_layers)
37
+ });
38
+ }
39
+ return {
40
+ ignored,
41
+ errors: ignored.map((item) => `ignored_unregistered_evidence: unregistered assertion JSON ${item.path} status=passed is not proof`)
42
+ };
43
+ }
44
+ async function readJson(file) {
45
+ try {
46
+ return JSON.parse(await readText(file));
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ }
52
+ function assertionRecord(value) {
53
+ if (!isRecord(value)) {
54
+ return undefined;
55
+ }
56
+ if (looksLikeAssertion(value)) {
57
+ return value;
58
+ }
59
+ if (isRecord(value.assertion_result) && looksLikeAssertion(value.assertion_result)) {
60
+ return value.assertion_result;
61
+ }
62
+ return undefined;
63
+ }
64
+ function looksLikeAssertion(value) {
65
+ return /assertion-result-v\d+/i.test(String(value.schema_version ?? "")) || (typeof value.status === "string" && Array.isArray(value.target_ac_ids));
66
+ }
67
+ function asStringArray(value) {
68
+ return Array.isArray(value) ? value.map(String).filter(Boolean) : [];
69
+ }
70
+ function slash(value) {
71
+ return value.replace(/\\/g, "/");
72
+ }
@@ -12,6 +12,7 @@ import { loadSuperpowersState, sha256 } from "./superpowers-task-state.js";
12
12
  import { validateCanonicalStatuses } from "./superpowers-task-status.js";
13
13
  import { isRecord } from "./superpowers-task-state-schema.js";
14
14
  import { evaluateProofLayerAssertions, isUiBrowserLayer, proofLayerName } from "./superpowers-task-assertions.js";
15
+ import { completionOutputContractFromState, completionPhraseFindingMessages, scanGeneratedCompletionOutputSurfaces } from "./superpowers-task-completion-output.js";
15
16
  export async function validateSuperpowersState(projectRoot, args = []) {
16
17
  const info = [];
17
18
  const warnings = [];
@@ -47,12 +48,52 @@ export async function validateSuperpowersState(projectRoot, args = []) {
47
48
  validateAuditor(state, errors);
48
49
  validateFinalCompletion(state, errors);
49
50
  errors.push(...(await derivedMatchesState(targetDir, state)));
51
+ await validateCompletionOutputConsistency(targetDir, state, errors, info);
50
52
  info.push(`checked superpowers task state ${repoRelative(projectRoot, targetDir)} plan_items=${Object.keys(state.graph?.plan_items ?? {}).length} acs=${Object.keys(state.graph?.acceptance_criteria ?? {}).length} evidence=${state.evidence?.length ?? 0}`);
51
53
  if (errors.length === 0) {
52
54
  info.push("Superpowers task state validation passed");
53
55
  }
54
56
  return { info, warnings, hygiene, errors };
55
57
  }
58
+ async function validateCompletionOutputConsistency(workdir, state, errors, info) {
59
+ const contract = completionOutputContractFromState(state);
60
+ const finalRecord = state.final;
61
+ const gate = isRecord(state.gates?.final_gate) ? state.gates.final_gate : {};
62
+ const storedStatus = finalRecord.completion_output_status ?? (typeof gate.completion_output_status === "string" ? gate.completion_output_status : undefined);
63
+ if (storedStatus && storedStatus !== contract.completion_output_status) {
64
+ errors.push(`completion_output_status mismatch: expected ${contract.completion_output_status}, found ${storedStatus}`);
65
+ }
66
+ if (contract.completion_output_status === "accept" && state.final.product_goal_complete !== true) {
67
+ errors.push("completion_output_status=accept but product_goal_complete is not true");
68
+ }
69
+ if (finalRecord.final_answer_allowed !== undefined && finalRecord.final_answer_allowed !== contract.final_answer_allowed) {
70
+ errors.push(`final_answer_allowed mismatch: expected ${contract.final_answer_allowed}, found ${finalRecord.final_answer_allowed}`);
71
+ }
72
+ if (finalRecord.required_user_visible_status && finalRecord.required_user_visible_status !== contract.required_user_visible_status) {
73
+ errors.push(`required_user_visible_status mismatch: expected ${contract.required_user_visible_status}, found ${finalRecord.required_user_visible_status}`);
74
+ }
75
+ if (finalRecord.exit_code !== undefined && finalRecord.exit_code !== contract.exit_code) {
76
+ errors.push(`completion output exit_code mismatch: expected ${contract.exit_code}, found ${finalRecord.exit_code}`);
77
+ }
78
+ if (contract.blocker_triage) {
79
+ info.push(`blocker_triage category=${contract.blocker_triage.category} self_recoverable=${contract.blocker_triage.self_recoverable} next_action=${contract.blocker_triage.next_action}`);
80
+ }
81
+ errors.push(...completionPhraseFindingMessages(await scanGeneratedCompletionOutputSurfaces(workdir, contract)));
82
+ await validateMarkdownCompletionStatus(workdir, contract.completion_output_status, errors);
83
+ }
84
+ async function validateMarkdownCompletionStatus(workdir, expectedStatus, errors) {
85
+ for (const relative of ["derived/final-summary.md", "derived/final-card.md", "derived/local-audit.md"]) {
86
+ const file = path.join(workdir, ...relative.split("/"));
87
+ if (!(await pathExists(file))) {
88
+ continue;
89
+ }
90
+ const text = await readText(file);
91
+ const match = /^completion_output_status:\s*(\S+)\s*$/im.exec(text);
92
+ if (match && match[1] !== expectedStatus) {
93
+ errors.push(`${relative} completion_output_status mismatch: expected ${expectedStatus}, found ${match[1]}`);
94
+ }
95
+ }
96
+ }
56
97
  async function validateSourceHashes(workdir, state, errors) {
57
98
  for (const [key, source] of Object.entries(state.sources ?? {})) {
58
99
  const file = path.join(workdir, source.path);