project-tiny-context-harness 0.2.79 → 0.2.81

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 (39) hide show
  1. package/README.md +17 -15
  2. package/assets/README.md +17 -15
  3. package/assets/README.zh-CN.md +19 -9
  4. package/assets/agents/AGENTS_CORE.md +42 -42
  5. package/assets/github/harness.yml +1 -1
  6. package/assets/skills/composite-long-task-workflow/SKILL.md +218 -0
  7. package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +33 -0
  8. package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +26 -0
  9. package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +644 -0
  10. package/assets/skills/normal-long-task/SKILL.md +12 -12
  11. package/dist/commands/composite-long-task.d.ts +6 -0
  12. package/dist/commands/composite-long-task.js +103 -0
  13. package/dist/commands/index.js +5 -3
  14. package/dist/commands/superpowers.js +6 -87
  15. package/dist/lib/composite-long-task-renderer.d.ts +12 -0
  16. package/dist/lib/composite-long-task-renderer.js +109 -0
  17. package/dist/lib/superpowers-task-assertion-normalizers.d.ts +3 -0
  18. package/dist/lib/superpowers-task-assertion-normalizers.js +67 -0
  19. package/dist/lib/superpowers-task-assertions.d.ts +20 -0
  20. package/dist/lib/superpowers-task-assertions.js +242 -0
  21. package/dist/lib/superpowers-task-compile-diagnostics.d.ts +5 -0
  22. package/dist/lib/superpowers-task-compile-diagnostics.js +20 -0
  23. package/dist/lib/superpowers-task-compile-guards.d.ts +2 -0
  24. package/dist/lib/superpowers-task-compile-guards.js +66 -0
  25. package/dist/lib/superpowers-task-compile.js +26 -6
  26. package/dist/lib/superpowers-task-conformance.d.ts +2 -0
  27. package/dist/lib/superpowers-task-conformance.js +24 -0
  28. package/dist/lib/superpowers-task-derive.js +61 -5
  29. package/dist/lib/superpowers-task-gates.js +62 -6
  30. package/dist/lib/superpowers-task-source-compile.js +94 -14
  31. package/dist/lib/superpowers-task-source-parser.js +20 -18
  32. package/dist/lib/superpowers-task-state-schema.d.ts +61 -1
  33. package/dist/lib/superpowers-task-state.js +19 -3
  34. package/dist/lib/superpowers-task-status.d.ts +2 -0
  35. package/dist/lib/superpowers-task-status.js +14 -0
  36. package/dist/lib/superpowers-task-validator.d.ts +1 -0
  37. package/dist/lib/superpowers-task-validator.js +30 -4
  38. package/package.json +5 -5
  39. package/assets/skills/superpowers-long-task/SKILL.md +0 -612
@@ -1,7 +1,8 @@
1
1
  import { appendSuperpowersEvent } from "./superpowers-task-events.js";
2
2
  import { deriveSuperpowersArtifacts } from "./superpowers-task-derive.js";
3
+ import { validatePlanAcceptance } from "./plan-acceptance-validator.js";
3
4
  import { loadSuperpowersState, recomputeStatuses, saveSuperpowersState } from "./superpowers-task-state.js";
4
- import { allCompletionConditionsSatisfied, validateSuperpowersState } from "./superpowers-task-validator.js";
5
+ import { completionConditionErrors, validateSuperpowersState } from "./superpowers-task-validator.js";
5
6
  export async function runSliceGate(workdir, sliceId) {
6
7
  const state = await loadSuperpowersState(workdir);
7
8
  const slice = state.slices.find((item) => item.slice_id === sliceId);
@@ -30,18 +31,73 @@ export async function runFinalGate(workdir) {
30
31
  await saveSuperpowersState(workdir, state);
31
32
  await deriveSuperpowersArtifacts(workdir);
32
33
  const report = await validateSuperpowersState(workdir, [workdir]);
34
+ const acceptanceReport = await validatePlanAcceptance(workdir, [workdir]);
33
35
  const latest = await loadSuperpowersState(workdir);
34
- const complete = report.errors.length === 0 && allCompletionConditionsSatisfied(latest);
36
+ const completionErrors = completionConditionErrors(latest);
37
+ const errors = [...new Set([...report.errors, ...acceptanceReport.errors, ...completionErrors])];
38
+ const complete = errors.length === 0;
39
+ const acceptanceStatus = complete ? "complete" : acceptanceStatusForErrors(errors);
40
+ const nextRequiredActions = complete ? [] : nextActionsForErrors(errors);
35
41
  latest.final.product_goal_complete = complete;
36
42
  latest.meta.product_goal_complete = complete;
37
- latest.final.acceptance_target_status = complete ? "complete" : "partial";
43
+ latest.final.acceptance_target_status = acceptanceStatus;
38
44
  latest.meta.acceptance_target_status = latest.final.acceptance_target_status;
39
45
  latest.final.audit_task_complete = true;
40
46
  latest.meta.audit_task_complete = true;
41
- latest.final.completion_basis = complete ? ["all_required_acs_complete", "validator_passed", "auditor_no_blocker"] : [];
42
- latest.gates.validator = { status: report.errors.length === 0 ? "pass" : "blocked", errors: report.errors };
47
+ latest.final.completion_basis = complete
48
+ ? ["all_required_acs_complete", "validator_passed", "assertion_evidence_passed", "negative_evidence_scan_passed", "auditor_no_blocker"]
49
+ : [];
50
+ latest.final.next_required_actions = nextRequiredActions;
51
+ latest.gates.validator = { status: errors.length === 0 ? "pass" : "blocked", errors };
52
+ latest.gates.final_gate = {
53
+ status: complete ? "pass" : acceptanceStatus,
54
+ order: [
55
+ "derive",
56
+ "verification_before_completion_expected",
57
+ "validate_state",
58
+ "validate_derived",
59
+ "validate_plan_acceptance",
60
+ "auditor_blocker_scan",
61
+ "stale_overclaim_scope_scan",
62
+ "ac_evidence_assertion_gate",
63
+ "negative_evidence_scan_gate",
64
+ "compute_completion"
65
+ ],
66
+ errors,
67
+ next_required_actions: nextRequiredActions
68
+ };
43
69
  await saveSuperpowersState(workdir, latest);
44
70
  await deriveSuperpowersArtifacts(workdir);
45
71
  await appendSuperpowersEvent(workdir, "final_gate", { product_goal_complete: complete });
46
- return { product_goal_complete: complete, errors: report.errors };
72
+ return { product_goal_complete: complete, errors };
73
+ }
74
+ function acceptanceStatusForErrors(errors) {
75
+ const text = errors.join("\n");
76
+ if (/source hash mismatch|source_changed_requires_recompile|scope_conflict_requires_decision|auditor blocker|Context Delta coverage is unresolved/i.test(text)) {
77
+ return "blocked";
78
+ }
79
+ if (/invalid evidence|forbidden shortcut|negative evidence|stale evidence|raw secret|contains_secret|sibling substitution|assertion_result\.status=failed|assertion exit_code=|command_exit_code=|forbidden text/i.test(text)) {
80
+ return "invalidated";
81
+ }
82
+ return "partial";
83
+ }
84
+ function nextActionsForErrors(errors) {
85
+ return errors.slice(0, 12).map((error) => {
86
+ if (/source hash mismatch/i.test(error)) {
87
+ return `${error}; rerun ty-context composite-long-task compile <workdir> after approving source changes`;
88
+ }
89
+ if (/scope_conflict_requires_decision/i.test(error)) {
90
+ return `${error}; clarify Product / Plan / Checklist delivery scope before more implementation`;
91
+ }
92
+ if (/missing assertion result|not machine-backed/i.test(error)) {
93
+ return `${error}; add assertion_result evidence for the target AC/proof layer`;
94
+ }
95
+ if (/negative evidence|forbidden/i.test(error)) {
96
+ return `${error}; rerun the negative evidence scan and fix the contradictory owner-surface state`;
97
+ }
98
+ if (/derived\/.*does not match/i.test(error)) {
99
+ return `${error}; rerun ty-context composite-long-task derive <workdir>`;
100
+ }
101
+ return error;
102
+ });
47
103
  }
@@ -1,3 +1,5 @@
1
+ import { MACHINE_VERIFIABLE_PROOF_LAYERS, proofLayerName } from "./superpowers-task-assertions.js";
2
+ import { compileReportSuffix, missingCategory, throwCompileErrors } from "./superpowers-task-compile-diagnostics.js";
1
3
  import { fieldArray, fieldBoolean, fieldLine, fieldText, parseDocumentFields, parseHeadingDefinitions } from "./superpowers-task-source-parser.js";
2
4
  export const DEFAULT_LAYERS = ["code", "test"];
3
5
  const PRODUCT_DELIVERY_SCOPES = new Set(["system_capability_build", "representative_sample_validation", "full_population_operation", "mixed_scope_requires_boundary"]);
@@ -8,7 +10,15 @@ const PRODUCT_FIELDS = new Set([
8
10
  "full_population_required",
9
11
  "representative_samples_validate",
10
12
  "representative_samples_do_not_validate",
11
- "out_of_scope_backlog"
13
+ "out_of_scope_backlog",
14
+ "source_authority",
15
+ "product_goal",
16
+ "surface_ia_lock",
17
+ "decision_lock",
18
+ "context_delta",
19
+ "source_to_context_coverage",
20
+ "acceptance_semantics",
21
+ "impact"
12
22
  ]);
13
23
  const PLAN_FIELDS = new Set([
14
24
  "delivery_scope",
@@ -20,16 +30,50 @@ const PLAN_FIELDS = new Set([
20
30
  "forbidden_surfaces",
21
31
  "implementation_paths",
22
32
  "required_tests",
23
- "related_acs"
33
+ "related_acs",
34
+ "requirement_ref",
35
+ "decision_id",
36
+ "proof_layer_ids",
37
+ "api_schema_changes",
38
+ "state_machine",
39
+ "data_flow",
40
+ "worker_runtime_behavior",
41
+ "ui_ia_changes",
42
+ "migration_plan",
43
+ "evidence_artifacts",
44
+ "explicit_no_test_scope",
45
+ "non_completing_shortcuts",
46
+ "substitution_policy",
47
+ "drift_severity",
48
+ "partial_conditions",
49
+ "blockers",
50
+ "context_fact_refs"
24
51
  ]);
25
52
  const ACCEPTANCE_FIELDS = new Set([
53
+ "checklist_source",
26
54
  "acceptance_scope",
27
55
  "ac_validates",
28
56
  "ac_does_not_validate",
29
57
  "sample_boundary",
30
58
  "full_population_required",
31
59
  "related_plan_items",
32
- "required_proof_layers"
60
+ "required_proof_layers",
61
+ "ac_type",
62
+ "proof_chain",
63
+ "verification_method",
64
+ "fail_conditions",
65
+ "invalid_evidence",
66
+ "substitution_policy",
67
+ "missing_layer_downgrade",
68
+ "auditor_expectation",
69
+ "out_of_scope_na_approval_source",
70
+ "required_test_ids",
71
+ "explicit_no_test_scope",
72
+ "hard_blockers",
73
+ "validates_explanation",
74
+ "does_not_validate_explanation",
75
+ "final_evidence_expected",
76
+ "test_cases"
33
77
  ]);
34
78
  export function parseProductArchitectureScope(content, sourceFile) {
35
79
  const fields = parseDocumentFields(content, sourceFile, PRODUCT_FIELDS);
@@ -63,6 +107,11 @@ export function parsePlanItems(content, sourceFile) {
63
107
  forbidden_surfaces: optionalArray(fields, "forbidden_surfaces"),
64
108
  implementation_paths: optionalArray(fields, "implementation_paths"),
65
109
  required_tests: optionalArray(fields, "required_tests"),
110
+ proof_layer_ids: optionalArray(fields, "proof_layer_ids").map(normalizeLayer),
111
+ non_completing_shortcuts: optionalArray(fields, "non_completing_shortcuts"),
112
+ substitution_policy: optionalArray(fields, "substitution_policy"),
113
+ explicit_no_test_scope: fieldBoolean(fields, "explicit_no_test_scope") === true,
114
+ context_fact_refs: optionalArray(fields, "context_fact_refs"),
66
115
  status: "not_started",
67
116
  related_acs: optionalArray(fields, "related_acs").map((item) => item.toUpperCase()),
68
117
  required_proof_layers: []
@@ -77,6 +126,7 @@ export function parseAcceptanceCriteria(content, sourceFile) {
77
126
  for (const definition of parseHeadingDefinitions(content, { kind: "AC", sourceFile, allowedFields: ACCEPTANCE_FIELDS })) {
78
127
  const fields = definition.fields;
79
128
  const layers = optionalArray(fields, "required_proof_layers").map(normalizeLayer).filter(Boolean);
129
+ const requiredProofLayers = layers.length > 0 ? layers : DEFAULT_LAYERS;
80
130
  items[definition.id] = {
81
131
  scope: definition.title,
82
132
  source_file: definition.source_file,
@@ -88,7 +138,13 @@ export function parseAcceptanceCriteria(content, sourceFile) {
88
138
  sample_boundary: requireText(errors, definition.id, "sample_boundary", fields, sourceFile, definition.source_start_line),
89
139
  full_population_required: requireBoolean(errors, definition.id, "full_population_required", fields, sourceFile, definition.source_start_line),
90
140
  related_plan_items: optionalArray(fields, "related_plan_items").map((item) => item.toUpperCase()),
91
- required_proof_layers: layers.length > 0 ? layers : DEFAULT_LAYERS,
141
+ required_proof_layers: requiredProofLayers,
142
+ assertion_requirements: assertionRequirements(fields, requiredProofLayers),
143
+ required_test_ids: optionalArray(fields, "required_test_ids"),
144
+ fail_conditions: optionalArray(fields, "fail_conditions"),
145
+ invalid_evidence: optionalArray(fields, "invalid_evidence"),
146
+ final_evidence_expected: optionalArray(fields, "final_evidence_expected"),
147
+ explicit_no_test_scope: fieldBoolean(fields, "explicit_no_test_scope") === true,
92
148
  status: "not_run"
93
149
  };
94
150
  }
@@ -98,15 +154,41 @@ export function parseAcceptanceCriteria(content, sourceFile) {
98
154
  function normalizeLayer(value) {
99
155
  return value.trim().toLowerCase().replace(/[- ]+/g, "_");
100
156
  }
157
+ function assertionRequirements(fields, layers) {
158
+ const requiredTestIds = optionalArray(fields, "required_test_ids");
159
+ const positiveAssertions = unique([
160
+ ...fieldArray(fields, "ac_validates"),
161
+ ...requiredTestIds,
162
+ ...optionalArray(fields, "test_cases"),
163
+ ...optionalArray(fields, "verification_method"),
164
+ ...optionalArray(fields, "final_evidence_expected")
165
+ ]);
166
+ const negativeAssertions = unique([
167
+ ...fieldArray(fields, "ac_does_not_validate"),
168
+ ...optionalArray(fields, "fail_conditions"),
169
+ ...optionalArray(fields, "invalid_evidence")
170
+ ]);
171
+ return layers.map((layer) => {
172
+ const normalized = proofLayerName(normalizeLayer(layer));
173
+ return {
174
+ proof_layer: normalized,
175
+ required: true,
176
+ machine_blocking: MACHINE_VERIFIABLE_PROOF_LAYERS.has(normalized),
177
+ required_test_ids: requiredTestIds,
178
+ positive_assertions: positiveAssertions,
179
+ negative_assertions: negativeAssertions
180
+ };
181
+ });
182
+ }
101
183
  function requireEnum(errors, label, name, fields, allowed, sourceFile, fallbackLine) {
102
184
  const value = fieldText(fields, name);
103
185
  const line = fieldLine(fields, name) ?? fallbackLine;
104
186
  if (!value) {
105
- errors.push(`${label} missing ${name} at ${sourceFile}:${line}`);
187
+ errors.push(`${label} missing ${name} at ${sourceFile}:${line}${compileReportSuffix(missingCategory(label), sourceFile, line, name, "field is required", "add the required field and rerun compile")}`);
106
188
  return "";
107
189
  }
108
190
  if (!allowed.has(value)) {
109
- errors.push(`${label} invalid ${name}: ${value} at ${sourceFile}:${line}; allowed: ${[...allowed].join(", ")}`);
191
+ errors.push(`${label} invalid ${name}: ${value} at ${sourceFile}:${line}; allowed: ${[...allowed].join(", ")}${compileReportSuffix("blocking_unparseable_object", sourceFile, line, name, `value must be one of ${[...allowed].join(", ")}`, "fix the field value and rerun compile")}`);
110
192
  }
111
193
  return value;
112
194
  }
@@ -114,14 +196,14 @@ function requireText(errors, label, name, fields, sourceFile, fallbackLine) {
114
196
  const value = fieldText(fields, name);
115
197
  const line = fieldLine(fields, name) ?? fallbackLine;
116
198
  if (!value) {
117
- errors.push(`${label} missing ${name} at ${sourceFile}:${line}`);
199
+ errors.push(`${label} missing ${name} at ${sourceFile}:${line}${compileReportSuffix(missingCategory(label), sourceFile, line, name, "field is required", "add non-empty text and rerun compile")}`);
118
200
  }
119
201
  return value;
120
202
  }
121
203
  function requireArray(errors, label, name, fields, sourceFile, fallbackLine) {
122
204
  const line = fieldLine(fields, name) ?? fallbackLine;
123
205
  if (!fields[name]) {
124
- errors.push(`${label} missing ${name} at ${sourceFile}:${line}`);
206
+ errors.push(`${label} missing ${name} at ${sourceFile}:${line}${compileReportSuffix(missingCategory(label), sourceFile, line, name, "field is required", "add the list field and rerun compile")}`);
125
207
  return [];
126
208
  }
127
209
  return fieldArray(fields, name);
@@ -129,20 +211,18 @@ function requireArray(errors, label, name, fields, sourceFile, fallbackLine) {
129
211
  function requireBoolean(errors, label, name, fields, sourceFile, fallbackLine) {
130
212
  const line = fieldLine(fields, name) ?? fallbackLine;
131
213
  if (!fields[name]) {
132
- errors.push(`${label} missing ${name} at ${sourceFile}:${line}`);
214
+ errors.push(`${label} missing ${name} at ${sourceFile}:${line}${compileReportSuffix(missingCategory(label), sourceFile, line, name, "field is required", "add true or false and rerun compile")}`);
133
215
  return null;
134
216
  }
135
217
  const value = fieldBoolean(fields, name);
136
218
  if (value === null) {
137
- errors.push(`${label} invalid ${name}: ${fieldText(fields, name)} at ${sourceFile}:${line}; must be true or false`);
219
+ errors.push(`${label} invalid ${name}: ${fieldText(fields, name)} at ${sourceFile}:${line}; must be true or false${compileReportSuffix("blocking_unparseable_object", sourceFile, line, name, "value must be true or false", "fix the boolean and rerun compile")}`);
138
220
  }
139
221
  return value;
140
222
  }
141
223
  function optionalArray(fields, name) {
142
224
  return fields[name] ? fieldArray(fields, name) : [];
143
225
  }
144
- function throwCompileErrors(errors) {
145
- if (errors.length > 0) {
146
- throw new Error(`Superpowers source compile failed:\n- ${errors.join("\n- ")}`);
147
- }
226
+ function unique(values) {
227
+ return [...new Set(values.filter(Boolean))];
148
228
  }
@@ -1,3 +1,4 @@
1
+ import { compileError, throwCompileErrors } from "./superpowers-task-compile-diagnostics.js";
1
2
  export function parseHeadingDefinitions(content, options) {
2
3
  const lines = splitLines(content);
3
4
  const errors = [];
@@ -8,7 +9,7 @@ export function parseHeadingDefinitions(content, options) {
8
9
  for (const heading of headings) {
9
10
  const firstLine = seen.get(heading.id);
10
11
  if (firstLine !== undefined) {
11
- errors.push(`${heading.id} duplicate definition at ${options.sourceFile}:${firstLine} and ${options.sourceFile}:${heading.line}`);
12
+ errors.push(compileError(`${heading.id} duplicate definition at ${options.sourceFile}:${firstLine} and ${options.sourceFile}:${heading.line}`, "blocking_unparseable_object", options.sourceFile, heading.line, heading.id, "duplicate object ids make PI/AC graph references ambiguous", "keep exactly one heading definition for this id"));
12
13
  continue;
13
14
  }
14
15
  seen.set(heading.id, heading.line);
@@ -24,15 +25,15 @@ export function parseHeadingDefinitions(content, options) {
24
25
  });
25
26
  }
26
27
  if (definitions.length === 0) {
27
- errors.push(`${options.sourceFile} must define ${options.kind} items with Markdown headings like "## ${options.kind}-001: ..."`);
28
+ errors.push(compileError(`${options.sourceFile} must define ${options.kind} items with Markdown headings like "## ${options.kind}-001: ..."`, options.kind === "PI" ? "blocking_missing_plan" : "blocking_missing_checklist", options.sourceFile, 1, `${options.kind}_heading`, "the source cannot compile without at least one object heading", `add headings like ## ${options.kind}-001: ...`));
28
29
  }
29
- throwIfErrors(errors);
30
+ throwCompileErrors(errors);
30
31
  return definitions;
31
32
  }
32
33
  export function parseDocumentFields(content, sourceFile, allowedFields) {
33
34
  const errors = [];
34
35
  const fields = parseFields(splitLines(content), sourceFile, 1, allowedFields, errors);
35
- throwIfErrors(errors);
36
+ throwCompileErrors(errors);
36
37
  return fields;
37
38
  }
38
39
  export function fieldText(fields, name) {
@@ -72,11 +73,11 @@ function parseFields(lines, sourceFile, startLine, allowedFields, errors) {
72
73
  const lineNumber = startLine + index;
73
74
  const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
74
75
  if (heading && allowedFields.has(heading[2].trim())) {
75
- errors.push(`${sourceFile}:${lineNumber} field headings are not supported; use "${heading[2].trim()}: ..."`);
76
+ errors.push(compileError(`${sourceFile}:${lineNumber} field headings are not supported; use "${heading[2].trim()}: ..."`, "blocking_unparseable_object", sourceFile, lineNumber, heading[2].trim(), "field heading syntax is ambiguous in compiled source", "use key: value fields"));
76
77
  continue;
77
78
  }
78
79
  if (/^\s*\|/.test(line) && containsKnownField(line, allowedFields)) {
79
- errors.push(`${sourceFile}:${lineNumber} table fields are not supported; use key: value fields`);
80
+ errors.push(compileError(`${sourceFile}:${lineNumber} table fields are not supported; use key: value fields`, "blocking_unparseable_object", sourceFile, lineNumber, "table_field", "table fields cannot be parsed into canonical state safely", "use key: value fields"));
80
81
  continue;
81
82
  }
82
83
  const match = /^([a-z][a-z0-9_]*)\s*:\s*(.*)$/.exec(line);
@@ -86,12 +87,12 @@ function parseFields(lines, sourceFile, startLine, allowedFields, errors) {
86
87
  const name = match[1];
87
88
  if (!allowedFields.has(name)) {
88
89
  if (name.includes("_")) {
89
- errors.push(`${sourceFile}:${lineNumber} unknown field ${name}`);
90
+ errors.push(compileError(`${sourceFile}:${lineNumber} unknown field ${name}`, "blocking_unparseable_object", sourceFile, lineNumber, name, "unknown fields may hide required source semantics", "rename the field to a supported canonical key"));
90
91
  }
91
92
  continue;
92
93
  }
93
94
  if (fields[name]) {
94
- errors.push(`${sourceFile}:${lineNumber} duplicate field ${name}`);
95
+ errors.push(compileError(`${sourceFile}:${lineNumber} duplicate field ${name}`, "blocking_unparseable_object", sourceFile, lineNumber, name, "duplicate fields make canonical source values ambiguous", "keep one field value"));
95
96
  continue;
96
97
  }
97
98
  const parsed = parseFieldValue(lines, index, match[2], sourceFile, lineNumber, errors);
@@ -107,13 +108,13 @@ function parseFieldValue(lines, index, rest, sourceFile, lineNumber, errors) {
107
108
  let cursor = index + 1;
108
109
  for (; cursor < lines.length; cursor++) {
109
110
  const next = lines[cursor];
110
- if (next.trim() && !/^\s/.test(next)) {
111
+ if (next.trim() && isTopLevelFieldOrHeading(next)) {
111
112
  break;
112
113
  }
113
114
  block.push(next.replace(/^\s{0,2}/, ""));
114
115
  }
115
116
  if (block.length === 0) {
116
- errors.push(`${sourceFile}:${lineNumber} block field must include indented text`);
117
+ errors.push(compileError(`${sourceFile}:${lineNumber} block field must include indented text`, "blocking_unparseable_object", sourceFile, lineNumber, "block_field", "empty block field cannot prove source intent", "add indented text or remove the field"));
117
118
  }
118
119
  return { value: block.join("\n").trim(), endIndex: cursor - 1 };
119
120
  }
@@ -127,18 +128,24 @@ function parseFieldValue(lines, index, rest, sourceFile, lineNumber, errors) {
127
128
  if (!next.trim()) {
128
129
  continue;
129
130
  }
130
- if (!/^\s/.test(next)) {
131
+ if (isTopLevelFieldOrHeading(next)) {
131
132
  break;
132
133
  }
133
134
  const listItem = /^\s*[-*+]\s+(.+?)\s*$/.exec(next);
134
135
  if (!listItem) {
135
- errors.push(`${sourceFile}:${cursor + 1} field lists must use indented "- item" entries or key: | blocks`);
136
+ errors.push(compileError(`${sourceFile}:${cursor + 1} field lists must use indented "- item" entries or key: | blocks`, "blocking_unparseable_object", sourceFile, cursor + 1, "list_field", "list syntax cannot be parsed into canonical arrays", "use indented '- item' entries or key: | blocks"));
136
137
  continue;
137
138
  }
138
139
  values.push(cleanValue(listItem[1]));
139
140
  }
140
141
  return { value: values, endIndex: cursor - 1 };
141
142
  }
143
+ function isTopLevelFieldOrHeading(line) {
144
+ if (/^(#{1,6})\s+/.test(line)) {
145
+ return true;
146
+ }
147
+ return /^([a-z][a-z0-9_]*)\s*:/.test(line);
148
+ }
142
149
  function rejectListStyleDefinitions(lines, options, errors) {
143
150
  const idPattern = options.kind === "PI" ? "PI" : "AC";
144
151
  const listPattern = new RegExp(`^\\s*[-*+]\\s+(${idPattern}-\\d{3,})\\b\\s*[:.-]?`, "i");
@@ -152,7 +159,7 @@ function rejectListStyleDefinitions(lines, options, errors) {
152
159
  continue;
153
160
  }
154
161
  const id = match[1].toUpperCase();
155
- errors.push(`${id} list-style definition is not allowed at ${options.sourceFile}:${index + 1}; use "## ${id}: ..."`);
162
+ errors.push(compileError(`${id} list-style definition is not allowed at ${options.sourceFile}:${index + 1}; use "## ${id}: ..."`, "blocking_unparseable_object", options.sourceFile, index + 1, id, "list-style object definitions are not canonical PI/AC headings", `use "## ${id}: ..."`));
156
163
  }
157
164
  }
158
165
  function listItemBlock(lines, start) {
@@ -211,8 +218,3 @@ function splitLines(content) {
211
218
  function cleanValue(value) {
212
219
  return value.replace(/^[-#*\s]+/, "").trim();
213
220
  }
214
- function throwIfErrors(errors) {
215
- if (errors.length > 0) {
216
- throw new Error(`Superpowers source compile failed:\n- ${errors.join("\n- ")}`);
217
- }
218
- }
@@ -54,7 +54,7 @@ export declare const SUPERPOWERS_TASK_STATE_JSON_SCHEMA: {
54
54
  };
55
55
  };
56
56
  export type SuperpowersProofLayerStatus = "missing" | "satisfied" | "invalidated" | "blocked";
57
- export type SuperpowersPlanItemStatus = "not_started" | "complete" | "partial" | "sampled_only" | "not_implemented" | "blocked" | "scope_changed_requires_user_approval" | "contradicted_by_current_state" | "out_of_scope_NA";
57
+ export type SuperpowersPlanItemStatus = "not_started" | "in_progress" | "complete" | "partial" | "blocked" | "invalidated" | "out_of_scope_NA";
58
58
  export type SuperpowersAcceptanceStatus = "not_run" | "complete" | "partial" | "blocked" | "invalidated" | "out_of_scope_NA";
59
59
  export type SuperpowersProductDeliveryScope = "system_capability_build" | "representative_sample_validation" | "full_population_operation" | "mixed_scope_requires_boundary";
60
60
  export type SuperpowersPlanDeliveryScope = "system_capability_build" | "representative_sample_validation" | "full_population_operation" | "out_of_scope_backlog";
@@ -95,6 +95,7 @@ export interface SuperpowersTaskState {
95
95
  acceptance_target_status: string;
96
96
  audit_task_complete: boolean;
97
97
  completion_basis: string[];
98
+ next_required_actions?: string[];
98
99
  };
99
100
  }
100
101
  export interface SuperpowersSourceRecord {
@@ -134,6 +135,11 @@ export interface SuperpowersPlanItem {
134
135
  forbidden_surfaces: string[];
135
136
  implementation_paths: string[];
136
137
  required_tests: string[];
138
+ proof_layer_ids?: string[];
139
+ non_completing_shortcuts?: string[];
140
+ substitution_policy?: string[];
141
+ explicit_no_test_scope?: boolean;
142
+ context_fact_refs?: string[];
137
143
  status: SuperpowersPlanItemStatus;
138
144
  related_acs: string[];
139
145
  required_proof_layers: string[];
@@ -150,8 +156,22 @@ export interface SuperpowersAcceptanceCriterion {
150
156
  full_population_required: boolean | null;
151
157
  related_plan_items: string[];
152
158
  required_proof_layers: string[];
159
+ assertion_requirements?: AssertionRequirement[];
160
+ required_test_ids?: string[];
161
+ fail_conditions?: string[];
162
+ invalid_evidence?: string[];
163
+ final_evidence_expected?: string[];
164
+ explicit_no_test_scope?: boolean;
153
165
  status: SuperpowersAcceptanceStatus;
154
166
  }
167
+ export interface AssertionRequirement {
168
+ proof_layer: string;
169
+ required: boolean;
170
+ machine_blocking: boolean;
171
+ required_test_ids: string[];
172
+ positive_assertions: string[];
173
+ negative_assertions: string[];
174
+ }
155
175
  export interface SuperpowersProofLayer {
156
176
  required: boolean;
157
177
  status: SuperpowersProofLayerStatus;
@@ -190,6 +210,7 @@ export interface SuperpowersEvidenceRecord {
190
210
  stale_after: string | null;
191
211
  };
192
212
  command?: string;
213
+ command_exit_code?: number;
193
214
  artifact_paths: string[];
194
215
  proves: string[];
195
216
  does_not_prove: string[];
@@ -201,8 +222,47 @@ export interface SuperpowersEvidenceRecord {
201
222
  external_reviewer_can_reproduce: boolean;
202
223
  reproduction_steps: string;
203
224
  };
225
+ assertion_result?: AssertionResult;
226
+ negative_evidence_scan?: NegativeEvidenceScan;
204
227
  sibling_substitution_used?: boolean;
205
228
  sibling_substitution_approval_source?: string;
206
229
  }
230
+ export interface AssertionResult {
231
+ schema_version: "assertion-result-v1";
232
+ status: "passed" | "failed" | "blocked" | "stale";
233
+ runner: string;
234
+ exit_code: number;
235
+ target_ac_ids: string[];
236
+ target_proof_layers: string[];
237
+ owner_surface?: string;
238
+ route?: string;
239
+ action?: string;
240
+ positive_assertions: AssertionCheck[];
241
+ negative_assertions: AssertionCheck[];
242
+ artifacts?: string[];
243
+ }
244
+ export interface AssertionCheck {
245
+ id: string;
246
+ status: "passed" | "failed" | "blocked" | "stale";
247
+ actual?: string;
248
+ expected?: string;
249
+ forbidden_text?: string;
250
+ }
251
+ export interface NegativeEvidenceScan {
252
+ schema_version: "negative-evidence-scan-v1";
253
+ status: "passed" | "failed" | "blocked" | "stale";
254
+ target_ac_ids: string[];
255
+ owner_surface?: string;
256
+ route?: string;
257
+ forbidden_findings: NegativeFinding[];
258
+ required_findings: AssertionCheck[];
259
+ artifacts: string[];
260
+ }
261
+ export interface NegativeFinding {
262
+ id: string;
263
+ status: "found" | "not_found";
264
+ forbidden_text?: string;
265
+ actual?: string;
266
+ }
207
267
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
208
268
  export declare function asStringArray(value: unknown): string[];
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { ensureDir, pathExists, readText, writeTextIfChanged } from "./fs.js";
4
4
  import { appendSuperpowersEvent } from "./superpowers-task-events.js";
5
+ import { evaluateProofLayerAssertions, isMachineVerifiableLayer, normalizeAssertionResult, normalizeNegativeEvidenceScan } from "./superpowers-task-assertions.js";
5
6
  import { SUPERPOWERS_TASK_STATE_JSON_SCHEMA, SUPERPOWERS_TASK_STATE_SCHEMA_VERSION, asStringArray, isRecord } from "./superpowers-task-state-schema.js";
6
7
  const SOURCE_FILES = {
7
8
  product_architecture_source: {
@@ -17,6 +18,7 @@ const SOURCE_FILES = {
17
18
  authority: "acs_completion_semantics_proof_layers"
18
19
  }
19
20
  };
21
+ const SLICE_PROGRESS_TYPES = new Set(["functional_gap_closed", "proof_gap_closed", "blocker_resolved", "invalid_evidence_removed"]);
20
22
  export async function initializeSuperpowersTask(workdir, options = {}) {
21
23
  await ensureDir(path.join(workdir, "derived"));
22
24
  await writeTextIfChanged(path.join(workdir, "task-state.schema.json"), `${stableJson(SUPERPOWERS_TASK_STATE_JSON_SCHEMA)}\n`);
@@ -71,7 +73,8 @@ export async function initializeSuperpowersTask(workdir, options = {}) {
71
73
  product_goal_complete: false,
72
74
  acceptance_target_status: "not_run",
73
75
  audit_task_complete: false,
74
- completion_basis: []
76
+ completion_basis: [],
77
+ next_required_actions: []
75
78
  }
76
79
  };
77
80
  await saveSuperpowersState(workdir, state);
@@ -96,6 +99,10 @@ export async function applySliceDelta(workdir, deltaFile) {
96
99
  if (!progressValue || !String(progressValue.type ?? "").trim() || asStringArray(progressValue.closed_items).length === 0) {
97
100
  throw new Error("slice_delta must include progress_value with type and closed_items");
98
101
  }
102
+ const progressType = String(progressValue.type);
103
+ if (!SLICE_PROGRESS_TYPES.has(progressType)) {
104
+ throw new Error(`slice_delta progress_value.type must be one of ${[...SLICE_PROGRESS_TYPES].join(", ")}; got ${progressType || "(missing)"}`);
105
+ }
99
106
  const evidenceRecords = readEvidenceRecords(delta.evidence_records);
100
107
  for (const evidence of evidenceRecords) {
101
108
  const existingIndex = state.evidence.findIndex((item) => item.evidence_id === evidence.evidence_id);
@@ -119,7 +126,7 @@ export async function applySliceDelta(workdir, deltaFile) {
119
126
  blockers: Array.isArray(delta.blockers) ? delta.blockers : [],
120
127
  cleanup_assertions: asStringArray(delta.cleanup_assertions),
121
128
  progress_value: {
122
- type: String(progressValue.type),
129
+ type: progressType,
123
130
  closed_items: asStringArray(progressValue.closed_items),
124
131
  why_it_reduces_rework: String(progressValue.why_it_reduces_rework ?? "")
125
132
  }
@@ -147,7 +154,7 @@ export function recomputeStatuses(state) {
147
154
  if (layerIds.length === 0) {
148
155
  ac.status = "not_run";
149
156
  }
150
- else if (layerIds.every((layerId) => state.graph.proof_layers[layerId]?.status === "satisfied")) {
157
+ else if (layerIds.every((layerId) => layerSatisfiedForCompletion(state, layerId))) {
151
158
  ac.status = "complete";
152
159
  }
153
160
  else if (layerIds.some((layerId) => state.graph.proof_layers[layerId]?.status === "satisfied")) {
@@ -169,6 +176,12 @@ export function recomputeStatuses(state) {
169
176
  }
170
177
  }
171
178
  }
179
+ function layerSatisfiedForCompletion(state, layerId) {
180
+ if (state.graph.proof_layers[layerId]?.status !== "satisfied") {
181
+ return false;
182
+ }
183
+ return !isMachineVerifiableLayer(layerId) || evaluateProofLayerAssertions(state, layerId).assertion_status === "passed";
184
+ }
172
185
  export function emptyProgressState() {
173
186
  return {
174
187
  system_capability_progress: { status: "not_started" },
@@ -208,6 +221,7 @@ function readEvidenceRecords(value) {
208
221
  }
209
222
  : { created_at: "", valid_for: "", stale_after: null },
210
223
  command: item.command === undefined ? undefined : String(item.command),
224
+ command_exit_code: item.command_exit_code === undefined ? undefined : Number(item.command_exit_code),
211
225
  artifact_paths: asStringArray(item.artifact_paths),
212
226
  proves: asStringArray(item.proves),
213
227
  does_not_prove: asStringArray(item.does_not_prove),
@@ -220,6 +234,8 @@ function readEvidenceRecords(value) {
220
234
  reproduction_steps: String(item.reviewability.reproduction_steps ?? "")
221
235
  }
222
236
  : { external_reviewer_can_reproduce: false, reproduction_steps: "" },
237
+ assertion_result: normalizeAssertionResult(item.assertion_result),
238
+ negative_evidence_scan: normalizeNegativeEvidenceScan(item.negative_evidence_scan),
223
239
  sibling_substitution_used: item.sibling_substitution_used === true,
224
240
  sibling_substitution_approval_source: item.sibling_substitution_approval_source === undefined ? undefined : String(item.sibling_substitution_approval_source)
225
241
  }));
@@ -0,0 +1,2 @@
1
+ import type { SuperpowersTaskState } from "./superpowers-task-state-schema.js";
2
+ export declare function validateCanonicalStatuses(state: SuperpowersTaskState, errors: string[]): void;
@@ -0,0 +1,14 @@
1
+ const CANONICAL_PLAN_STATUSES = new Set(["not_started", "in_progress", "partial", "blocked", "invalidated", "complete", "out_of_scope_NA"]);
2
+ const CANONICAL_AC_STATUSES = new Set(["not_started", "not_run", "in_progress", "partial", "blocked", "invalidated", "complete", "out_of_scope_NA"]);
3
+ export function validateCanonicalStatuses(state, errors) {
4
+ for (const [planId, item] of Object.entries(state.graph?.plan_items ?? {})) {
5
+ if (!CANONICAL_PLAN_STATUSES.has(item.status)) {
6
+ errors.push(`plan item ${planId} has non-canonical status: ${item.status}`);
7
+ }
8
+ }
9
+ for (const [acId, ac] of Object.entries(state.graph?.acceptance_criteria ?? {})) {
10
+ if (!CANONICAL_AC_STATUSES.has(ac.status)) {
11
+ errors.push(`AC ${acId} has non-canonical status: ${ac.status}`);
12
+ }
13
+ }
14
+ }
@@ -1,4 +1,5 @@
1
1
  import { type SuperpowersTaskState } from "./superpowers-task-state-schema.js";
2
2
  import type { ValidatorReport } from "./validators.js";
3
3
  export declare function validateSuperpowersState(projectRoot: string, args?: string[]): Promise<ValidatorReport>;
4
+ export declare function completionConditionErrors(state: SuperpowersTaskState): string[];
4
5
  export declare function allCompletionConditionsSatisfied(state: SuperpowersTaskState): boolean;