project-tiny-context-harness 0.2.80 → 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 (30) hide show
  1. package/README.md +8 -8
  2. package/assets/README.md +7 -7
  3. package/assets/README.zh-CN.md +8 -0
  4. package/assets/github/harness.yml +1 -1
  5. package/assets/skills/composite-long-task-workflow/SKILL.md +8 -2
  6. package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +2 -0
  7. package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +6 -2
  8. package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +22 -11
  9. package/dist/lib/composite-long-task-renderer.js +18 -62
  10. package/dist/lib/superpowers-task-assertion-normalizers.d.ts +3 -0
  11. package/dist/lib/superpowers-task-assertion-normalizers.js +67 -0
  12. package/dist/lib/superpowers-task-assertions.d.ts +20 -0
  13. package/dist/lib/superpowers-task-assertions.js +242 -0
  14. package/dist/lib/superpowers-task-compile-diagnostics.d.ts +5 -0
  15. package/dist/lib/superpowers-task-compile-diagnostics.js +20 -0
  16. package/dist/lib/superpowers-task-compile-guards.d.ts +2 -0
  17. package/dist/lib/superpowers-task-compile-guards.js +66 -0
  18. package/dist/lib/superpowers-task-compile.js +25 -5
  19. package/dist/lib/superpowers-task-conformance.d.ts +2 -0
  20. package/dist/lib/superpowers-task-conformance.js +24 -0
  21. package/dist/lib/superpowers-task-derive.js +60 -4
  22. package/dist/lib/superpowers-task-gates.js +57 -3
  23. package/dist/lib/superpowers-task-source-compile.js +49 -11
  24. package/dist/lib/superpowers-task-source-parser.js +12 -16
  25. package/dist/lib/superpowers-task-state-schema.d.ts +61 -1
  26. package/dist/lib/superpowers-task-state.js +19 -3
  27. package/dist/lib/superpowers-task-status.d.ts +2 -0
  28. package/dist/lib/superpowers-task-status.js +14 -0
  29. package/dist/lib/superpowers-task-validator.js +14 -1
  30. package/package.json +5 -5
@@ -1,5 +1,6 @@
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
5
  import { completionConditionErrors, validateSuperpowersState } from "./superpowers-task-validator.js";
5
6
  export async function runSliceGate(workdir, sliceId) {
@@ -30,20 +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
36
  const completionErrors = completionConditionErrors(latest);
35
- const errors = [...new Set([...report.errors, ...completionErrors])];
37
+ const errors = [...new Set([...report.errors, ...acceptanceReport.errors, ...completionErrors])];
36
38
  const complete = errors.length === 0;
39
+ const acceptanceStatus = complete ? "complete" : acceptanceStatusForErrors(errors);
40
+ const nextRequiredActions = complete ? [] : nextActionsForErrors(errors);
37
41
  latest.final.product_goal_complete = complete;
38
42
  latest.meta.product_goal_complete = complete;
39
- latest.final.acceptance_target_status = complete ? "complete" : "partial";
43
+ latest.final.acceptance_target_status = acceptanceStatus;
40
44
  latest.meta.acceptance_target_status = latest.final.acceptance_target_status;
41
45
  latest.final.audit_task_complete = true;
42
46
  latest.meta.audit_task_complete = true;
43
- latest.final.completion_basis = complete ? ["all_required_acs_complete", "validator_passed", "auditor_no_blocker"] : [];
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;
44
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
+ };
45
69
  await saveSuperpowersState(workdir, latest);
46
70
  await deriveSuperpowersArtifacts(workdir);
47
71
  await appendSuperpowersEvent(workdir, "final_gate", { product_goal_complete: complete });
48
72
  return { product_goal_complete: complete, errors };
49
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
+ });
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"]);
@@ -105,6 +107,11 @@ export function parsePlanItems(content, sourceFile) {
105
107
  forbidden_surfaces: optionalArray(fields, "forbidden_surfaces"),
106
108
  implementation_paths: optionalArray(fields, "implementation_paths"),
107
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"),
108
115
  status: "not_started",
109
116
  related_acs: optionalArray(fields, "related_acs").map((item) => item.toUpperCase()),
110
117
  required_proof_layers: []
@@ -119,6 +126,7 @@ export function parseAcceptanceCriteria(content, sourceFile) {
119
126
  for (const definition of parseHeadingDefinitions(content, { kind: "AC", sourceFile, allowedFields: ACCEPTANCE_FIELDS })) {
120
127
  const fields = definition.fields;
121
128
  const layers = optionalArray(fields, "required_proof_layers").map(normalizeLayer).filter(Boolean);
129
+ const requiredProofLayers = layers.length > 0 ? layers : DEFAULT_LAYERS;
122
130
  items[definition.id] = {
123
131
  scope: definition.title,
124
132
  source_file: definition.source_file,
@@ -130,7 +138,13 @@ export function parseAcceptanceCriteria(content, sourceFile) {
130
138
  sample_boundary: requireText(errors, definition.id, "sample_boundary", fields, sourceFile, definition.source_start_line),
131
139
  full_population_required: requireBoolean(errors, definition.id, "full_population_required", fields, sourceFile, definition.source_start_line),
132
140
  related_plan_items: optionalArray(fields, "related_plan_items").map((item) => item.toUpperCase()),
133
- 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,
134
148
  status: "not_run"
135
149
  };
136
150
  }
@@ -140,15 +154,41 @@ export function parseAcceptanceCriteria(content, sourceFile) {
140
154
  function normalizeLayer(value) {
141
155
  return value.trim().toLowerCase().replace(/[- ]+/g, "_");
142
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
+ }
143
183
  function requireEnum(errors, label, name, fields, allowed, sourceFile, fallbackLine) {
144
184
  const value = fieldText(fields, name);
145
185
  const line = fieldLine(fields, name) ?? fallbackLine;
146
186
  if (!value) {
147
- 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")}`);
148
188
  return "";
149
189
  }
150
190
  if (!allowed.has(value)) {
151
- 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")}`);
152
192
  }
153
193
  return value;
154
194
  }
@@ -156,14 +196,14 @@ function requireText(errors, label, name, fields, sourceFile, fallbackLine) {
156
196
  const value = fieldText(fields, name);
157
197
  const line = fieldLine(fields, name) ?? fallbackLine;
158
198
  if (!value) {
159
- 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")}`);
160
200
  }
161
201
  return value;
162
202
  }
163
203
  function requireArray(errors, label, name, fields, sourceFile, fallbackLine) {
164
204
  const line = fieldLine(fields, name) ?? fallbackLine;
165
205
  if (!fields[name]) {
166
- 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")}`);
167
207
  return [];
168
208
  }
169
209
  return fieldArray(fields, name);
@@ -171,20 +211,18 @@ function requireArray(errors, label, name, fields, sourceFile, fallbackLine) {
171
211
  function requireBoolean(errors, label, name, fields, sourceFile, fallbackLine) {
172
212
  const line = fieldLine(fields, name) ?? fallbackLine;
173
213
  if (!fields[name]) {
174
- 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")}`);
175
215
  return null;
176
216
  }
177
217
  const value = fieldBoolean(fields, name);
178
218
  if (value === null) {
179
- 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")}`);
180
220
  }
181
221
  return value;
182
222
  }
183
223
  function optionalArray(fields, name) {
184
224
  return fields[name] ? fieldArray(fields, name) : [];
185
225
  }
186
- function throwCompileErrors(errors) {
187
- if (errors.length > 0) {
188
- throw new Error(`Superpowers source compile failed:\n- ${errors.join("\n- ")}`);
189
- }
226
+ function unique(values) {
227
+ return [...new Set(values.filter(Boolean))];
190
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);
@@ -113,7 +114,7 @@ function parseFieldValue(lines, index, rest, sourceFile, lineNumber, errors) {
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
  }
@@ -132,7 +133,7 @@ function parseFieldValue(lines, index, rest, sourceFile, lineNumber, errors) {
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]));
@@ -158,7 +159,7 @@ function rejectListStyleDefinitions(lines, options, errors) {
158
159
  continue;
159
160
  }
160
161
  const id = match[1].toUpperCase();
161
- 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}: ..."`));
162
163
  }
163
164
  }
164
165
  function listItemBlock(lines, start) {
@@ -217,8 +218,3 @@ function splitLines(content) {
217
218
  function cleanValue(value) {
218
219
  return value.replace(/^[-#*\s]+/, "").trim();
219
220
  }
220
- function throwIfErrors(errors) {
221
- if (errors.length > 0) {
222
- throw new Error(`Superpowers source compile failed:\n- ${errors.join("\n- ")}`);
223
- }
224
- }
@@ -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
+ }
@@ -3,9 +3,12 @@ import { pathExists, readText } from "./fs.js";
3
3
  import { findSensitiveEvidence } from "./plan-acceptance-evidence.js";
4
4
  import { primitiveText, repoRelative, resolveInputDir } from "./plan-validator-common.js";
5
5
  import { derivedMatchesState } from "./superpowers-task-derive.js";
6
+ import { validatePlanCompletionConformance } from "./superpowers-task-conformance.js";
6
7
  import { fullPopulationRequired, validateDeliveryContract, validateScopeConflicts } from "./superpowers-task-delivery.js";
7
8
  import { loadSuperpowersState, sha256 } from "./superpowers-task-state.js";
9
+ import { validateCanonicalStatuses } from "./superpowers-task-status.js";
8
10
  import { isRecord } from "./superpowers-task-state-schema.js";
11
+ import { evaluateProofLayerAssertions, isUiBrowserLayer } from "./superpowers-task-assertions.js";
9
12
  export async function validateSuperpowersState(projectRoot, args = []) {
10
13
  const info = [];
11
14
  const warnings = [];
@@ -35,7 +38,9 @@ export async function validateSuperpowersState(projectRoot, args = []) {
35
38
  }
36
39
  await validateSourceHashes(targetDir, state, errors);
37
40
  validateDeliveryContract(state, errors);
41
+ validateCanonicalStatuses(state, errors);
38
42
  validateGraphReferences(state, errors);
43
+ validatePlanCompletionConformance(state, errors);
39
44
  validateScopeConflicts(state, errors);
40
45
  validateEvidenceRecords(state, errors);
41
46
  validateProofLayers(state, errors);
@@ -163,7 +168,7 @@ function validateEvidenceRecords(state, errors) {
163
168
  if (proofLayer.endsWith(".runtime") && /\b(mock|unit|viewmodel)\b/i.test(evidence.type)) {
164
169
  errors.push(`${label} runtime proof cannot be mock/unit/viewmodel only`);
165
170
  }
166
- if (proofLayer.endsWith(".ui_browser") && !/\b(browser|ui_browser|screenshot)\b/i.test(evidence.type)) {
171
+ if (proofLayer.endsWith(".ui_browser") && !/(browser|ui_browser|screenshot|playwright)/i.test(evidence.type)) {
167
172
  errors.push(`${label} UI proof must use browser owner surface evidence`);
168
173
  }
169
174
  }
@@ -185,6 +190,13 @@ function validateProofLayers(state, errors) {
185
190
  errors.push(`proof layer ${layerId} references ${evidenceId} but that evidence does not prove it`);
186
191
  }
187
192
  }
193
+ if (layer.status === "satisfied") {
194
+ const evaluation = evaluateProofLayerAssertions(state, layerId);
195
+ errors.push(...evaluation.blocking_assertion_failures, ...evaluation.negative_evidence_findings);
196
+ if (isUiBrowserLayer(layerId) && evaluation.assertion_status !== "passed") {
197
+ errors.push(`proof layer ${layerId} ui_browser proof not machine-backed`);
198
+ }
199
+ }
188
200
  }
189
201
  }
190
202
  function validateAuditor(state, errors) {
@@ -242,6 +254,7 @@ export function completionConditionErrors(state) {
242
254
  validateDeliveryContract(state, errors);
243
255
  validateScopeConflicts(state, errors);
244
256
  validateGraphReferences(state, errors);
257
+ validatePlanCompletionConformance(state, errors);
245
258
  validateEvidenceRecords(state, errors);
246
259
  validateProofLayers(state, errors);
247
260
  validateAuditor(state, errors);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-tiny-context-harness",
3
- "version": "0.2.80",
3
+ "version": "0.2.81",
4
4
  "description": "Minimal project memory and validation harness for AI coding agents.",
5
5
  "license": "MIT",
6
6
  "author": "Seven128",
@@ -55,15 +55,15 @@
55
55
  "prepack": "npm run build"
56
56
  },
57
57
  "engines": {
58
- "node": ">=20"
58
+ "node": ">=24"
59
59
  },
60
60
  "dependencies": {
61
- "@google/design.md": "^0.2.0",
62
- "impeccable": "^2.3.2",
61
+ "@google/design.md": "^0.3.0",
62
+ "impeccable": "^3.1.0",
63
63
  "yaml": "^2.9.0"
64
64
  },
65
65
  "devDependencies": {
66
- "@types/node": "^24.0.0",
66
+ "@types/node": "^26.0.0",
67
67
  "typescript": "^5.5.0"
68
68
  }
69
69
  }