project-tiny-context-harness 0.2.81 → 0.2.83

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 (52) hide show
  1. package/README.md +11 -3
  2. package/assets/README.md +11 -3
  3. package/assets/README.zh-CN.md +9 -1
  4. package/assets/protected-harness-baseline.json +18 -0
  5. package/assets/skills/composite-long-task-workflow/SKILL.md +21 -1
  6. package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +10 -0
  7. package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +6 -15
  8. package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +32 -15
  9. package/dist/commands/composite-long-task.js +45 -2
  10. package/dist/lib/superpowers-task-ac010.d.ts +6 -0
  11. package/dist/lib/superpowers-task-ac010.js +26 -0
  12. package/dist/lib/superpowers-task-assertion-normalizers.js +8 -1
  13. package/dist/lib/superpowers-task-assertions.js +35 -20
  14. package/dist/lib/superpowers-task-attempt.d.ts +4 -0
  15. package/dist/lib/superpowers-task-attempt.js +102 -0
  16. package/dist/lib/superpowers-task-command-specs.d.ts +3 -0
  17. package/dist/lib/superpowers-task-command-specs.js +52 -0
  18. package/dist/lib/superpowers-task-compile.d.ts +4 -1
  19. package/dist/lib/superpowers-task-compile.js +7 -1
  20. package/dist/lib/superpowers-task-contradictions.d.ts +6 -0
  21. package/dist/lib/superpowers-task-contradictions.js +126 -0
  22. package/dist/lib/superpowers-task-current-evidence.d.ts +3 -0
  23. package/dist/lib/superpowers-task-current-evidence.js +154 -0
  24. package/dist/lib/superpowers-task-delivery.js +30 -18
  25. package/dist/lib/superpowers-task-derive.d.ts +1 -0
  26. package/dist/lib/superpowers-task-derive.js +76 -1
  27. package/dist/lib/superpowers-task-evidence-kernel.d.ts +12 -0
  28. package/dist/lib/superpowers-task-evidence-kernel.js +351 -0
  29. package/dist/lib/superpowers-task-evidence-records.d.ts +2 -0
  30. package/dist/lib/superpowers-task-evidence-records.js +55 -0
  31. package/dist/lib/superpowers-task-evidence.d.ts +10 -0
  32. package/dist/lib/superpowers-task-evidence.js +141 -0
  33. package/dist/lib/superpowers-task-fields.d.ts +22 -0
  34. package/dist/lib/superpowers-task-fields.js +276 -0
  35. package/dist/lib/superpowers-task-gates.js +36 -24
  36. package/dist/lib/superpowers-task-harness-drift.d.ts +11 -0
  37. package/dist/lib/superpowers-task-harness-drift.js +86 -0
  38. package/dist/lib/superpowers-task-protected-baseline.d.ts +10 -0
  39. package/dist/lib/superpowers-task-protected-baseline.js +47 -0
  40. package/dist/lib/superpowers-task-source-compile.js +93 -95
  41. package/dist/lib/superpowers-task-source-parser.js +1 -3
  42. package/dist/lib/superpowers-task-state-schema.d.ts +158 -3
  43. package/dist/lib/superpowers-task-state-schema.js +23 -1
  44. package/dist/lib/superpowers-task-state-shape.d.ts +3 -0
  45. package/dist/lib/superpowers-task-state-shape.js +50 -0
  46. package/dist/lib/superpowers-task-state.js +19 -39
  47. package/dist/lib/superpowers-task-status.js +11 -1
  48. package/dist/lib/superpowers-task-under-specified.d.ts +7 -0
  49. package/dist/lib/superpowers-task-under-specified.js +61 -0
  50. package/dist/lib/superpowers-task-validator.js +11 -37
  51. package/package.json +69 -69
  52. package/source-mappings.yaml +3 -0
@@ -1,16 +1,7 @@
1
+ import { evaluateCurrentAttemptEvidence } from "./superpowers-task-current-evidence.js";
2
+ import { MACHINE_VERIFIABLE_LAYER_NAMES, normalizeProofLayerId, normalizeProofLayerName } from "./superpowers-task-fields.js";
1
3
  export { normalizeAssertionResult, normalizeNegativeEvidenceScan } from "./superpowers-task-assertion-normalizers.js";
2
- export const MACHINE_VERIFIABLE_PROOF_LAYERS = new Set([
3
- "ui_browser",
4
- "api_schema",
5
- "runtime",
6
- "worker_runtime",
7
- "data_artifact",
8
- "integration",
9
- "security_redaction",
10
- "test",
11
- "all_provider_all_runner",
12
- "cleanup_stale_scan"
13
- ]);
4
+ export const MACHINE_VERIFIABLE_PROOF_LAYERS = new Set(MACHINE_VERIFIABLE_LAYER_NAMES);
14
5
  export const UI_BROWSER_ASSERTION_TYPES = new Set(["browser_assertion", "playwright_assertion", "ui_browser_assertion"]);
15
6
  const DEFAULT_UI_FORBIDDEN_FINAL_STATES = ["未验证", "不可用", "暂不可用", "页面无明显变化"];
16
7
  const GENERIC_INVALID_EVIDENCE_TYPE_PATTERNS = [
@@ -33,7 +24,8 @@ const GENERIC_INVALID_EVIDENCE_TYPE_PATTERNS = [
33
24
  /\bsample[-_ ]?for[-_ ]?full[-_ ]?population\b/i
34
25
  ];
35
26
  export function proofLayerName(layerId) {
36
- return layerId.includes(".") ? layerId.slice(layerId.lastIndexOf(".") + 1) : layerId;
27
+ const raw = layerId.includes(".") ? layerId.slice(layerId.lastIndexOf(".") + 1) : layerId;
28
+ return normalizeProofLayerName(raw);
37
29
  }
38
30
  export function proofLayerAcId(layerId) {
39
31
  return layerId.includes(".") ? layerId.slice(0, layerId.lastIndexOf(".")) : "";
@@ -76,6 +68,13 @@ export function evaluateProofLayerAssertions(state, layerId) {
76
68
  if (!isMachineVerifiableLayer(layerId)) {
77
69
  return { assertion_status: "not_applicable", blocking_assertion_failures: [], negative_evidence_findings: [] };
78
70
  }
71
+ if (!state.current_attempt_id) {
72
+ return {
73
+ assertion_status: "missing",
74
+ blocking_assertion_failures: [`proof layer ${layerId} missing current attempt; current attempt is required for machine-backed completion`],
75
+ negative_evidence_findings: []
76
+ };
77
+ }
79
78
  const layer = state.graph.proof_layers[layerId];
80
79
  const evidenceById = new Map((state.evidence ?? []).map((item) => [item.evidence_id, item]));
81
80
  const evidenceRecords = (layer?.evidence_ids ?? []).map((id) => evidenceById.get(id)).filter((item) => Boolean(item));
@@ -89,6 +88,7 @@ export function evaluateProofLayerAssertions(state, layerId) {
89
88
  const blocking = [];
90
89
  const negative = [];
91
90
  for (const evidence of evidenceRecords) {
91
+ blocking.push(...evaluateCurrentAttemptEvidence(state, evidence, layerId));
92
92
  blocking.push(...evaluateAssertionEvidence(evidence, layerId));
93
93
  negative.push(...evaluateNegativeEvidence(evidence, layerId));
94
94
  }
@@ -124,8 +124,8 @@ export function evaluateAssertionEvidence(evidence, layerId) {
124
124
  failures.push(`${label} missing assertion result; ${layerName} proof not machine-backed`);
125
125
  return failures;
126
126
  }
127
- if (assertion.schema_version !== "assertion-result-v1") {
128
- failures.push(`${label} assertion_result.schema_version must be assertion-result-v1`);
127
+ if (assertion.schema_version !== "assertion-result-v1" && assertion.schema_version !== "assertion-result-v2") {
128
+ failures.push(`${label} assertion_result.schema_version must be assertion-result-v1 or assertion-result-v2`);
129
129
  }
130
130
  if (assertion.status !== "passed") {
131
131
  failures.push(`${label} assertion_result.status=${assertion.status}; expected passed`);
@@ -139,11 +139,18 @@ export function evaluateAssertionEvidence(evidence, layerId) {
139
139
  if (!assertion.target_ac_ids.includes(acId)) {
140
140
  failures.push(`${label} assertion target ACs ${assertion.target_ac_ids.join(", ") || "(none)"} do not include ${acId}`);
141
141
  }
142
- if (!assertion.target_proof_layers.includes(layerId) && !assertion.target_proof_layers.includes(layerName)) {
142
+ const assertionTargetLayers = assertion.target_proof_layers.map(normalizeProofLayerId);
143
+ const normalizedLayerId = normalizeProofLayerId(layerId);
144
+ if (!assertionTargetLayers.includes(normalizedLayerId) && !assertionTargetLayers.includes(layerName)) {
143
145
  failures.push(`${label} assertion target_proof_layers ${assertion.target_proof_layers.join(", ") || "(none)"} do not include ${layerId} or ${layerName}`);
144
146
  }
145
147
  failures.push(...checkAssertions(`${label} positive assertion`, assertion.positive_assertions));
146
148
  failures.push(...checkAssertions(`${label} negative assertion`, assertion.negative_assertions));
149
+ failures.push(...checkAssertions(`${label} invalid completion signal`, assertion.invalid_completion_signals ?? []));
150
+ const missingRequiredTests = (assertion.required_test_ids ?? []).filter((testId) => !testId);
151
+ if (missingRequiredTests.length > 0) {
152
+ failures.push(`${label} assertion_result.required_test_ids contains empty test ids`);
153
+ }
147
154
  if ((assertion.artifacts?.length ?? 0) === 0 && evidence.artifact_paths.length === 0) {
148
155
  failures.push(`${label} assertion-backed evidence must include artifacts`);
149
156
  }
@@ -189,9 +196,9 @@ function checkAssertions(prefix, checks) {
189
196
  .map((check) => `${prefix} ${check.id || "(unnamed)"} status=${check.status}; expected passed${check.forbidden_text ? ` forbidden_text=${check.forbidden_text}` : ""}`);
190
197
  }
191
198
  export function evaluateNegativeEvidence(evidence, layerId) {
192
- const scan = evidence.negative_evidence_scan;
199
+ const scan = evidence.assertion_result?.negative_evidence_scan ?? evidence.negative_evidence_scan;
193
200
  if (!scan) {
194
- return [];
201
+ return [`proof layer ${layerId} evidence ${evidence.evidence_id} missing negative_evidence_scan`];
195
202
  }
196
203
  const acId = proofLayerAcId(layerId);
197
204
  const label = `proof layer ${layerId} evidence ${evidence.evidence_id}`;
@@ -205,6 +212,14 @@ export function evaluateNegativeEvidence(evidence, layerId) {
205
212
  if (!scan.target_ac_ids.includes(acId)) {
206
213
  findings.push(`${label} negative evidence scan target ACs ${scan.target_ac_ids.join(", ") || "(none)"} do not include ${acId}`);
207
214
  }
215
+ const targetLayers = (scan.target_proof_layers ?? []).map(normalizeProofLayerId);
216
+ const normalizedLayerId = normalizeProofLayerId(layerId);
217
+ if (targetLayers.length === 0) {
218
+ findings.push(`${label} negative evidence scan target proof layers are missing; expected ${normalizedLayerId}`);
219
+ }
220
+ else if (!targetLayers.includes(normalizedLayerId) && !targetLayers.includes(proofLayerName(layerId))) {
221
+ findings.push(`${label} negative evidence scan target proof layers ${targetLayers.join(", ") || "(none)"} do not include ${normalizedLayerId}`);
222
+ }
208
223
  for (const finding of scan.forbidden_findings ?? []) {
209
224
  if (finding.status === "found") {
210
225
  findings.push(`${label} negative evidence found forbidden text ${finding.forbidden_text ?? finding.id}: ${finding.actual ?? ""}`.trim());
@@ -226,7 +241,7 @@ function invalidEvidenceReason(evidence, layerName) {
226
241
  if (genericShortcut) {
227
242
  return `matches invalid completion evidence ${genericShortcut}`;
228
243
  }
229
- if ((layerName === "runtime" || layerName === "worker_runtime" || layerName === "integration" || layerName === "ui_browser") && /\b(unit|mock|viewmodel)\b/i.test(evidence.type)) {
244
+ if ((layerName === "worker_runtime" || layerName === "integration" || layerName === "ui_browser") && /\b(unit|mock|viewmodel)\b/i.test(evidence.type)) {
230
245
  return "unit, mock or viewmodel evidence is auxiliary only for runtime, worker, integration and UI layers";
231
246
  }
232
247
  if (layerName === "ui_browser" && /\b(api|schema)\b/i.test(evidence.type)) {
@@ -235,7 +250,7 @@ function invalidEvidenceReason(evidence, layerName) {
235
250
  if (layerName === "ui_browser" && /^screenshot$/i.test(evidence.type)) {
236
251
  return "screenshot-only evidence cannot satisfy UI Path AC";
237
252
  }
238
- if ((layerName === "runtime" || layerName === "worker_runtime" || layerName === "data_artifact" || layerName === "security_redaction") && /\b(ui|browser|playwright|screenshot)\b/i.test(evidence.type)) {
253
+ if ((layerName === "worker_runtime" || layerName === "data_artifact" || layerName === "security_redaction") && /\b(ui|browser|playwright|screenshot)\b/i.test(evidence.type)) {
239
254
  return "UI-only evidence cannot satisfy runtime, data or security proof layers";
240
255
  }
241
256
  return undefined;
@@ -0,0 +1,4 @@
1
+ import type { ExecutionAttempt, SuperpowersAttemptMode, SuperpowersTaskState } from "./superpowers-task-state-schema.js";
2
+ export declare function startSuperpowersAttempt(workdir: string, state: SuperpowersTaskState, mode?: SuperpowersAttemptMode): Promise<ExecutionAttempt>;
3
+ export declare function startAndSaveSuperpowersAttempt(workdir: string, mode?: SuperpowersAttemptMode): Promise<ExecutionAttempt>;
4
+ export declare function computeSourceBundleHash(state: SuperpowersTaskState, specsHash?: string): string;
@@ -0,0 +1,102 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { appendSuperpowersEvent } from "./superpowers-task-events.js";
4
+ import { requiredCommandSpecsHash } from "./superpowers-task-command-specs.js";
5
+ import { loadSuperpowersState, saveSuperpowersState, sha256, stableJson } from "./superpowers-task-state.js";
6
+ const execFileAsync = promisify(execFile);
7
+ export async function startSuperpowersAttempt(workdir, state, mode = "product_task") {
8
+ const now = new Date().toISOString();
9
+ const specsHash = requiredCommandSpecsHash(state.required_command_specs ?? []);
10
+ const sourceBundleHash = computeSourceBundleHash(state, specsHash);
11
+ const git = await readGitIdentity(workdir);
12
+ const worktreeFingerprint = sha256(stableJson(git));
13
+ const attemptOrdinal = (state.attempts ?? []).length + 1;
14
+ const attempt = {
15
+ task_attempt_id: `ATT-${compactDate(now)}-${attemptOrdinal}-${sourceBundleHash.slice(0, 8)}`,
16
+ source_bundle_hash: sourceBundleHash,
17
+ product_source_hash: state.sources.product_architecture_source?.sha256 ?? "",
18
+ technical_plan_hash: state.sources.technical_realization_plan?.sha256 ?? "",
19
+ acceptance_checklist_hash: state.sources.acceptance_checklist?.sha256 ?? "",
20
+ git_head: git.git_head,
21
+ git_status_short: git.git_status_short,
22
+ tracked_diff_hash: git.tracked_diff_hash,
23
+ relevant_untracked_hash: git.relevant_untracked_hash,
24
+ untracked_relevant_hash: git.relevant_untracked_hash,
25
+ worktree_fingerprint: worktreeFingerprint,
26
+ started_at: now,
27
+ ended_at: null,
28
+ finalized_at: null,
29
+ required_command_specs_hash: specsHash,
30
+ mode,
31
+ changed_files: git.changed_files
32
+ };
33
+ state.attempts = [...(state.attempts ?? []).filter((item) => item.task_attempt_id !== attempt.task_attempt_id), attempt];
34
+ state.current_attempt_id = attempt.task_attempt_id;
35
+ return attempt;
36
+ }
37
+ export async function startAndSaveSuperpowersAttempt(workdir, mode = "product_task") {
38
+ const state = await loadSuperpowersState(workdir);
39
+ const attempt = await startSuperpowersAttempt(workdir, state, mode);
40
+ await saveSuperpowersState(workdir, state);
41
+ await appendSuperpowersEvent(workdir, "attempt_started", { task_attempt_id: attempt.task_attempt_id });
42
+ return attempt;
43
+ }
44
+ export function computeSourceBundleHash(state, specsHash = requiredCommandSpecsHash(state.required_command_specs ?? [])) {
45
+ return sha256(stableJson({
46
+ product_source_hash: state.sources.product_architecture_source?.sha256 ?? "",
47
+ technical_plan_hash: state.sources.technical_realization_plan?.sha256 ?? "",
48
+ acceptance_checklist_hash: state.sources.acceptance_checklist?.sha256 ?? "",
49
+ required_command_specs_hash: specsHash
50
+ }));
51
+ }
52
+ async function readGitIdentity(workdir) {
53
+ const head = await git(workdir, ["rev-parse", "HEAD"]);
54
+ if (!head.ok) {
55
+ return {
56
+ git_head: "no-git",
57
+ git_status_short: "no-git",
58
+ tracked_diff_hash: sha256("no-git-tracked-diff"),
59
+ relevant_untracked_hash: "ignored:no-git",
60
+ changed_files: []
61
+ };
62
+ }
63
+ const status = await git(workdir, ["status", "--short"]);
64
+ const diff = await git(workdir, ["diff", "--binary"]);
65
+ const stagedDiff = await git(workdir, ["diff", "--cached", "--binary"]);
66
+ const untracked = await git(workdir, ["ls-files", "--others", "--exclude-standard"]);
67
+ const trackedNames = await git(workdir, ["diff", "--name-only"]);
68
+ const stagedNames = await git(workdir, ["diff", "--cached", "--name-only"]);
69
+ const changedFiles = unique([
70
+ ...lines(trackedNames.ok ? trackedNames.stdout : ""),
71
+ ...lines(stagedNames.ok ? stagedNames.stdout : ""),
72
+ ...lines(untracked.ok ? untracked.stdout : "")
73
+ ]);
74
+ return {
75
+ git_head: head.stdout.trim(),
76
+ git_status_short: status.ok ? status.stdout.trim() : "git-status-unavailable",
77
+ tracked_diff_hash: sha256(`${diff.ok ? diff.stdout : "git-diff-unavailable"}\n${stagedDiff.ok ? stagedDiff.stdout : ""}`),
78
+ relevant_untracked_hash: untracked.ok && untracked.stdout.trim() ? sha256(untracked.stdout.trim()) : "none",
79
+ changed_files: changedFiles
80
+ };
81
+ }
82
+ async function git(workdir, args) {
83
+ try {
84
+ const result = await execFileAsync("git", args, { cwd: workdir, windowsHide: true, timeout: 10000 });
85
+ return { ok: true, stdout: result.stdout };
86
+ }
87
+ catch {
88
+ return { ok: false, stdout: "" };
89
+ }
90
+ }
91
+ function compactDate(value) {
92
+ return value.replace(/[-:.TZ]/g, "").slice(0, 14);
93
+ }
94
+ function lines(value) {
95
+ return value
96
+ .split(/\r?\n/)
97
+ .map((item) => item.trim().replace(/\\/g, "/"))
98
+ .filter(Boolean);
99
+ }
100
+ function unique(values) {
101
+ return [...new Set(values.filter(Boolean))];
102
+ }
@@ -0,0 +1,3 @@
1
+ import type { RequiredCommandSpec, SuperpowersTaskState } from "./superpowers-task-state-schema.js";
2
+ export declare function deriveRequiredCommandSpecs(state: SuperpowersTaskState): RequiredCommandSpec[];
3
+ export declare function requiredCommandSpecsHash(specs: RequiredCommandSpec[]): string;
@@ -0,0 +1,52 @@
1
+ import { createHash } from "node:crypto";
2
+ import { stableJson } from "./superpowers-task-state.js";
3
+ export function deriveRequiredCommandSpecs(state) {
4
+ return Object.entries(state.graph.acceptance_criteria).flatMap(([acId, ac]) => {
5
+ const proofLayers = [...new Set((ac.required_proof_layers ?? []).filter(Boolean))];
6
+ if (proofLayers.length === 0) {
7
+ return [];
8
+ }
9
+ const spec = {
10
+ ac_id: acId,
11
+ proof_layers: proofLayers,
12
+ command: ac.assertion_command ?? "",
13
+ assertion_artifacts: ac.assertion_artifacts ?? [],
14
+ required_test_ids: ac.required_test_ids ?? [],
15
+ machine_blocking: ac.machine_blocking !== false,
16
+ assertion_result_required: ac.assertion_result_required !== false,
17
+ positive_assertions: ac.positive_assertions ?? [],
18
+ negative_assertions: ac.negative_assertions ?? [],
19
+ invalid_completion_signals: ac.invalid_completion_signals ?? [],
20
+ final_evidence_expected: ac.final_evidence_expected ?? []
21
+ };
22
+ return [{ command_spec_id: commandSpecId(spec), ...spec }];
23
+ });
24
+ }
25
+ export function requiredCommandSpecsHash(specs) {
26
+ return sha256(stableJson(specs.map((spec) => ({
27
+ ...spec,
28
+ proof_layers: [...(spec.proof_layers ?? [])].sort(),
29
+ assertion_artifacts: [...(spec.assertion_artifacts ?? [])].sort(),
30
+ required_test_ids: [...(spec.required_test_ids ?? [])].sort(),
31
+ positive_assertions: [...(spec.positive_assertions ?? [])].sort(),
32
+ negative_assertions: [...(spec.negative_assertions ?? [])].sort(),
33
+ invalid_completion_signals: [...(spec.invalid_completion_signals ?? [])].sort(),
34
+ final_evidence_expected: [...(spec.final_evidence_expected ?? [])].sort()
35
+ }))));
36
+ }
37
+ function commandSpecId(spec) {
38
+ return sha256([
39
+ spec.ac_id,
40
+ spec.proof_layers.join(","),
41
+ spec.command,
42
+ spec.assertion_artifacts.join(","),
43
+ spec.required_test_ids.join(","),
44
+ spec.positive_assertions.join(","),
45
+ spec.negative_assertions.join(","),
46
+ spec.invalid_completion_signals.join(","),
47
+ spec.final_evidence_expected.join(",")
48
+ ].join("\n"));
49
+ }
50
+ function sha256(value) {
51
+ return createHash("sha256").update(value).digest("hex");
52
+ }
@@ -1,3 +1,6 @@
1
1
  import { type SuperpowersTaskState } from "./superpowers-task-state-schema.js";
2
- export declare function compileSuperpowersTask(workdir: string): Promise<SuperpowersTaskState>;
2
+ import type { SuperpowersAttemptMode } from "./superpowers-task-state-schema.js";
3
+ export declare function compileSuperpowersTask(workdir: string, options?: {
4
+ mode?: SuperpowersAttemptMode;
5
+ }): Promise<SuperpowersTaskState>;
3
6
  export declare function computeScopeConflicts(state: SuperpowersTaskState): string[];
@@ -3,9 +3,11 @@ import { pathExists, readText } from "./fs.js";
3
3
  import { appendSuperpowersEvent } from "./superpowers-task-events.js";
4
4
  import { compileError, compileReportSuffix, throwCompileErrors } from "./superpowers-task-compile-diagnostics.js";
5
5
  import { validateCompiledSources } from "./superpowers-task-compile-guards.js";
6
+ import { startSuperpowersAttempt } from "./superpowers-task-attempt.js";
7
+ import { deriveRequiredCommandSpecs } from "./superpowers-task-command-specs.js";
6
8
  import { loadSuperpowersState, recomputeStatuses, saveSuperpowersState, refreshSourceHashes } from "./superpowers-task-state.js";
7
9
  import { DEFAULT_LAYERS, parseAcceptanceCriteria, parsePlanItems, parseProductArchitectureScope } from "./superpowers-task-source-compile.js";
8
- export async function compileSuperpowersTask(workdir) {
10
+ export async function compileSuperpowersTask(workdir, options = {}) {
9
11
  const state = await loadSuperpowersState(workdir);
10
12
  await refreshSourceHashes(workdir, state);
11
13
  await assertRequiredSourcesExist(workdir, state);
@@ -40,9 +42,13 @@ export async function compileSuperpowersTask(workdir) {
40
42
  }
41
43
  }
42
44
  state.graph.edges = Object.entries(planItems).flatMap(([planId, item]) => item.related_acs.map((acId) => ({ from: planId, to: acId, type: "supports" })));
45
+ state.required_command_specs = deriveRequiredCommandSpecs(state);
46
+ state.command_runs = [];
47
+ state.negative_evidence_records = [];
43
48
  state.delivery.scope_conflicts = computeScopeConflicts(state);
44
49
  state.progress = compileProgress(state);
45
50
  recomputeStatuses(state);
51
+ await startSuperpowersAttempt(workdir, state, options.mode ?? "product_task");
46
52
  await saveSuperpowersState(workdir, state);
47
53
  await appendSuperpowersEvent(workdir, "graph_compiled", {
48
54
  plan_items: Object.keys(planItems).length,
@@ -0,0 +1,6 @@
1
+ import type { SuperpowersTaskState } from "./superpowers-task-state-schema.js";
2
+ export interface SuperpowersContradictionScan {
3
+ errors: string[];
4
+ historicalCompleteIgnored: boolean;
5
+ }
6
+ export declare function scanSuperpowersContradictions(workdir: string, state: SuperpowersTaskState): Promise<SuperpowersContradictionScan>;
@@ -0,0 +1,126 @@
1
+ import path from "node:path";
2
+ import { listFiles, pathExists, readText } from "./fs.js";
3
+ const OWNER_SURFACE_FORBIDDEN_STATES = ["尚未运行自测", "运行未记录", "未验证", "不可用", "暂不可用", "页面无明显变化"];
4
+ export async function scanSuperpowersContradictions(workdir, state) {
5
+ const files = await listFiles(workdir);
6
+ const errors = [
7
+ ...scanFinalStateContradictions(state),
8
+ ...(await scanCurrentFailureArtifacts(workdir, files))
9
+ ];
10
+ const historicalCompleteIgnored = errors.length > 0 && (await hasHistoricalCompleteEvent(workdir));
11
+ if (historicalCompleteIgnored) {
12
+ errors.push("historical_complete_ignored: Historical stale completion event detected and ignored. Current recomputed product_goal_complete=false.");
13
+ }
14
+ if (errors.length > 0) {
15
+ errors.push("workflow_gate_bug_prevented: current contradiction scan prevents product_goal_complete=true.");
16
+ }
17
+ return { errors: unique(errors), historicalCompleteIgnored };
18
+ }
19
+ function scanFinalStateContradictions(state) {
20
+ const errors = [];
21
+ if (state.meta?.product_goal_complete === false && state.final?.product_goal_complete === true) {
22
+ errors.push("current contradiction task_state_product_goal_mismatch: meta product_goal_complete=false but final product_goal_complete=true.");
23
+ }
24
+ if (state.meta?.acceptance_target_status && state.final?.acceptance_target_status && state.meta.acceptance_target_status !== state.final.acceptance_target_status) {
25
+ errors.push(`current contradiction task_state_acceptance_status_mismatch: meta=${state.meta.acceptance_target_status} final=${state.final.acceptance_target_status}.`);
26
+ }
27
+ for (const commandRun of state.command_runs ?? []) {
28
+ if (Number(commandRun.exit_code) !== 0) {
29
+ errors.push(`current contradiction command_run_failed: ${commandRun.command_run_id} ac=${commandRun.ac_id} proof_layer=${commandRun.proof_layer} exit_code=${commandRun.exit_code}.`);
30
+ }
31
+ }
32
+ return errors;
33
+ }
34
+ async function scanCurrentFailureArtifacts(workdir, files) {
35
+ const errors = [];
36
+ for (const file of files) {
37
+ const relative = slash(path.relative(workdir, file));
38
+ if (relative.endsWith(".last-run.json")) {
39
+ errors.push(...(await scanPlaywrightLastRun(file, relative)));
40
+ continue;
41
+ }
42
+ if (/test-results\/.*error-context\.md$/i.test(relative)) {
43
+ errors.push(...(await scanOwnerDomErrorContext(file, relative)));
44
+ continue;
45
+ }
46
+ if (isFailureJsonCandidate(relative)) {
47
+ errors.push(...(await scanFailedJsonArtifact(file, relative)));
48
+ }
49
+ }
50
+ return errors;
51
+ }
52
+ async function scanPlaywrightLastRun(file, relative) {
53
+ const parsed = await readJsonRecord(file);
54
+ if (!parsed) {
55
+ return [];
56
+ }
57
+ const status = String(parsed.status ?? parsed.outcome ?? "").toLowerCase();
58
+ const failedTests = Array.isArray(parsed.failedTests) ? parsed.failedTests : Array.isArray(parsed.failed_tests) ? parsed.failed_tests : [];
59
+ if (status !== "failed" && failedTests.length === 0) {
60
+ return [];
61
+ }
62
+ return [
63
+ `current contradiction playwright_last_run_failed: ${relative} status=${status || "unknown"} failed_tests=${failedTests
64
+ .map(String)
65
+ .join(", ") || "(unspecified)"}.`
66
+ ];
67
+ }
68
+ async function scanOwnerDomErrorContext(file, relative) {
69
+ const text = await readText(file);
70
+ const findings = OWNER_SURFACE_FORBIDDEN_STATES.filter((state) => text.includes(state));
71
+ if (findings.length === 0) {
72
+ return [];
73
+ }
74
+ return [`current contradiction owner_dom_forbidden_state: ${relative} contains ${findings.join(", ")}.`];
75
+ }
76
+ async function scanFailedJsonArtifact(file, relative) {
77
+ const parsed = await readJsonRecord(file);
78
+ if (!parsed) {
79
+ return [];
80
+ }
81
+ const text = JSON.stringify(parsed);
82
+ const status = String(parsed.status ?? parsed.outcome ?? parsed.result ?? "").toLowerCase();
83
+ const failures = Number(parsed.failures ?? parsed.failed ?? parsed.failed_count ?? 0);
84
+ if (status !== "failed" && failures <= 0 && !/"failed"/i.test(text)) {
85
+ return [];
86
+ }
87
+ return [`current contradiction failed_test_result_artifact: ${relative} reports failed status.`];
88
+ }
89
+ function isFailureJsonCandidate(relative) {
90
+ return (/\.json$/i.test(relative) &&
91
+ /(^|\/)(test-results|playwright-report|junit|command-runs|negative-evidence|negative_evidence)(\/|$)/i.test(relative));
92
+ }
93
+ async function hasHistoricalCompleteEvent(workdir) {
94
+ const eventsPath = path.join(workdir, "events.ndjson");
95
+ if (!(await pathExists(eventsPath))) {
96
+ return false;
97
+ }
98
+ const content = await readText(eventsPath);
99
+ return content
100
+ .split(/\r?\n/)
101
+ .filter(Boolean)
102
+ .some((line) => {
103
+ try {
104
+ const event = JSON.parse(line);
105
+ return String(event.event ?? event.event_type ?? event.type ?? "") === "final_gate" && event.product_goal_complete === true;
106
+ }
107
+ catch {
108
+ return /final_gate/.test(line) && /product_goal_complete["']?\s*:\s*true/.test(line);
109
+ }
110
+ });
111
+ }
112
+ async function readJsonRecord(file) {
113
+ try {
114
+ const parsed = JSON.parse(await readText(file));
115
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
116
+ }
117
+ catch {
118
+ return undefined;
119
+ }
120
+ }
121
+ function slash(value) {
122
+ return value.split(path.sep).join("/");
123
+ }
124
+ function unique(values) {
125
+ return [...new Set(values.filter(Boolean))];
126
+ }
@@ -0,0 +1,3 @@
1
+ import type { SuperpowersEvidenceRecord, SuperpowersTaskState } from "./superpowers-task-state-schema.js";
2
+ export declare function evaluateCurrentAttemptEvidence(state: SuperpowersTaskState, evidence: SuperpowersEvidenceRecord, layerId: string): string[];
3
+ export declare function evaluateCurrentAttemptArtifact(workdir: string, evidence: SuperpowersEvidenceRecord, layerId: string): Promise<string[]>;
@@ -0,0 +1,154 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { pathExists, readText } from "./fs.js";
4
+ import { normalizeProofLayerName } from "./superpowers-task-fields.js";
5
+ export function evaluateCurrentAttemptEvidence(state, evidence, layerId) {
6
+ if (!state.current_attempt_id) {
7
+ return [];
8
+ }
9
+ const failures = [];
10
+ const label = `proof layer ${layerId} evidence ${evidence.evidence_id}`;
11
+ if (evidence.schema_version !== "evidence-record-v2") {
12
+ failures.push(`${label} must be EvidenceRecordV2 for current-attempt machine completion`);
13
+ }
14
+ if (evidence.task_attempt_id !== state.current_attempt_id) {
15
+ failures.push(`${label} stale evidence from old attempt ${evidence.task_attempt_id || "(missing)"}; expected current attempt ${state.current_attempt_id}`);
16
+ }
17
+ for (const field of [
18
+ "source_bundle_hash",
19
+ "product_source_hash",
20
+ "technical_plan_hash",
21
+ "acceptance_checklist_hash",
22
+ "git_head",
23
+ "worktree_fingerprint",
24
+ "command_spec_id",
25
+ "command_run_id",
26
+ "command_line",
27
+ "artifact_path",
28
+ "artifact_sha256",
29
+ "artifact_mtime"
30
+ ]) {
31
+ if (!evidence[field]) {
32
+ failures.push(`${label} EvidenceRecordV2 missing ${field}`);
33
+ }
34
+ }
35
+ const attempt = state.attempts?.find((item) => item.task_attempt_id === state.current_attempt_id);
36
+ if (attempt && evidence.source_bundle_hash && evidence.source_bundle_hash !== attempt.source_bundle_hash) {
37
+ failures.push(`${label} stale evidence source_bundle_hash mismatch for current attempt`);
38
+ }
39
+ if (attempt && evidence.product_source_hash && evidence.product_source_hash !== attempt.product_source_hash) {
40
+ failures.push(`${label} stale evidence product_source_hash mismatch for current attempt`);
41
+ }
42
+ if (attempt && evidence.technical_plan_hash && evidence.technical_plan_hash !== attempt.technical_plan_hash) {
43
+ failures.push(`${label} stale evidence technical_plan_hash mismatch for current attempt`);
44
+ }
45
+ if (attempt && evidence.acceptance_checklist_hash && evidence.acceptance_checklist_hash !== attempt.acceptance_checklist_hash) {
46
+ failures.push(`${label} stale evidence acceptance_checklist_hash mismatch for current attempt`);
47
+ }
48
+ if (attempt && evidence.git_head && evidence.git_head !== attempt.git_head) {
49
+ failures.push(`${label} stale evidence git_head mismatch for current attempt`);
50
+ }
51
+ if (attempt && evidence.worktree_fingerprint && evidence.worktree_fingerprint !== attempt.worktree_fingerprint) {
52
+ failures.push(`${label} stale evidence worktree_fingerprint mismatch for current attempt`);
53
+ }
54
+ if (attempt && evidence.artifact_mtime && Date.parse(evidence.artifact_mtime) < Date.parse(attempt.started_at)) {
55
+ failures.push(`${label} stale evidence artifact_mtime predates current attempt`);
56
+ }
57
+ const commandRun = state.command_runs?.find((item) => item.command_run_id === evidence.command_run_id);
58
+ if (!commandRun) {
59
+ failures.push(`${label} missing command run ${evidence.command_run_id || "(missing)"}`);
60
+ }
61
+ else {
62
+ if (commandRun.task_attempt_id !== state.current_attempt_id) {
63
+ failures.push(`${label} command run ${commandRun.command_run_id} is from old attempt ${commandRun.task_attempt_id}`);
64
+ }
65
+ if (commandRun.exit_code !== 0) {
66
+ failures.push(`${label} command run ${commandRun.command_run_id} exit_code=${commandRun.exit_code}; expected 0`);
67
+ }
68
+ if (commandRun.command_spec_id !== evidence.command_spec_id) {
69
+ failures.push(`${label} command_spec_id mismatch between evidence and command run`);
70
+ }
71
+ }
72
+ const commandSpec = state.required_command_specs?.find((item) => item.command_spec_id === evidence.command_spec_id);
73
+ if (!commandSpec) {
74
+ failures.push(`${label} missing required command spec ${evidence.command_spec_id || "(missing)"}`);
75
+ }
76
+ else if (commandSpec.ac_id !== proofLayerAcId(layerId) || !commandSpec.proof_layers.map(normalizeProofLayerName).includes(proofLayerName(layerId))) {
77
+ failures.push(`${label} command spec does not cover ${layerId}`);
78
+ }
79
+ const ac = state.graph.acceptance_criteria?.[proofLayerAcId(layerId)];
80
+ const targetAcIds = evidence.target_ac_ids ?? evidence.assertion_result?.target_ac_ids ?? [];
81
+ if (!targetAcIds.includes(proofLayerAcId(layerId))) {
82
+ failures.push(`${label} target_ac_ids ${targetAcIds.join(", ") || "(none)"} do not include ${proofLayerAcId(layerId)}`);
83
+ }
84
+ const expectedPiIds = ac?.related_plan_items ?? [];
85
+ const targetPiIds = evidence.target_pi_ids ?? evidence.assertion_result?.target_pi_ids ?? [];
86
+ for (const piId of expectedPiIds) {
87
+ if (!targetPiIds.includes(piId)) {
88
+ failures.push(`${label} target_pi_ids ${targetPiIds.join(", ") || "(none)"} do not include related plan item ${piId}`);
89
+ }
90
+ }
91
+ const targetLayers = (evidence.target_proof_layers ?? evidence.assertion_result?.target_proof_layers ?? []).map(normalizeLayerId);
92
+ if (!targetLayers.includes(normalizeLayerId(layerId)) && !targetLayers.includes(proofLayerName(layerId))) {
93
+ failures.push(`${label} target_proof_layers ${targetLayers.join(", ") || "(none)"} do not include ${layerId}`);
94
+ }
95
+ if (evidence.assertion_result?.schema_version !== "assertion-result-v2") {
96
+ failures.push(`${label} assertion_result.schema_version must be assertion-result-v2 for current-attempt machine completion`);
97
+ }
98
+ return failures;
99
+ }
100
+ export async function evaluateCurrentAttemptArtifact(workdir, evidence, layerId) {
101
+ const label = `proof layer ${layerId} evidence ${evidence.evidence_id}`;
102
+ return validateArtifact(workdir, evidence, label);
103
+ }
104
+ async function validateArtifact(workdir, evidence, label) {
105
+ const failures = [];
106
+ if (!evidence.artifact_path) {
107
+ failures.push(`${label} EvidenceRecordV2 missing artifact_path`);
108
+ return failures;
109
+ }
110
+ const artifact = await resolveArtifactPath(workdir, evidence.artifact_path);
111
+ if (!artifact) {
112
+ failures.push(`${label} artifact_path does not exist: ${evidence.artifact_path}`);
113
+ return failures;
114
+ }
115
+ if (evidence.artifact_sha256) {
116
+ const actual = createHash("sha256").update(await readText(artifact)).digest("hex");
117
+ if (actual !== evidence.artifact_sha256) {
118
+ failures.push(`${label} stale evidence artifact_sha256 mismatch for ${evidence.artifact_path}`);
119
+ }
120
+ }
121
+ return failures;
122
+ }
123
+ async function resolveArtifactPath(workdir, artifactPath) {
124
+ const candidates = path.isAbsolute(artifactPath) ? [artifactPath] : [path.join(workdir, artifactPath), path.join(projectRootFromWorkdir(workdir), artifactPath)];
125
+ for (const candidate of candidates) {
126
+ if (await pathExists(candidate)) {
127
+ return candidate;
128
+ }
129
+ }
130
+ return undefined;
131
+ }
132
+ function proofLayerName(layerId) {
133
+ const raw = layerId.includes(".") ? layerId.slice(layerId.lastIndexOf(".") + 1) : layerId;
134
+ return normalizeProofLayerName(raw);
135
+ }
136
+ function proofLayerAcId(layerId) {
137
+ return layerId.includes(".") ? layerId.slice(0, layerId.lastIndexOf(".")) : "";
138
+ }
139
+ function normalizeLayerId(layerId) {
140
+ if (!layerId.includes(".")) {
141
+ return normalizeProofLayerName(layerId);
142
+ }
143
+ const acId = proofLayerAcId(layerId);
144
+ return `${acId}.${proofLayerName(layerId)}`;
145
+ }
146
+ function projectRootFromWorkdir(workdir) {
147
+ const normalized = workdir.replace(/\\/g, "/");
148
+ const marker = "/tmp/ty-context/plan-acceptance/";
149
+ const index = normalized.lastIndexOf(marker);
150
+ if (index < 0) {
151
+ return workdir;
152
+ }
153
+ return normalized.slice(0, index);
154
+ }