project-tiny-context-harness 0.2.83 → 0.2.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/assets/README.md +8 -8
- package/assets/README.zh-CN.md +4 -4
- package/assets/protected-harness-baseline.json +6 -4
- package/assets/skills/composite-long-task-workflow/SKILL.md +12 -5
- package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +19 -0
- package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +2 -2
- package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +47 -26
- package/dist/commands/composite-long-task.js +21 -3
- package/dist/lib/package-source.js +11 -1
- package/dist/lib/superpowers-task-command-run-correlation.d.ts +8 -0
- package/dist/lib/superpowers-task-command-run-correlation.js +103 -0
- package/dist/lib/superpowers-task-completion-output.d.ts +91 -0
- package/dist/lib/superpowers-task-completion-output.js +374 -0
- package/dist/lib/superpowers-task-current-evidence.js +22 -0
- package/dist/lib/superpowers-task-derive.js +66 -8
- package/dist/lib/superpowers-task-evidence-kernel.d.ts +7 -0
- package/dist/lib/superpowers-task-evidence-kernel.js +67 -71
- package/dist/lib/superpowers-task-evidence.js +7 -1
- package/dist/lib/superpowers-task-final-card.d.ts +3 -0
- package/dist/lib/superpowers-task-final-card.js +35 -0
- package/dist/lib/superpowers-task-gates.d.ts +2 -2
- package/dist/lib/superpowers-task-gates.js +179 -27
- package/dist/lib/superpowers-task-harness-drift.js +6 -2
- package/dist/lib/superpowers-task-protected-baseline.js +21 -2
- package/dist/lib/superpowers-task-state-schema.d.ts +37 -0
- package/dist/lib/superpowers-task-state.js +10 -1
- package/dist/lib/superpowers-task-unregistered-evidence.d.ts +11 -0
- package/dist/lib/superpowers-task-unregistered-evidence.js +72 -0
- package/dist/lib/superpowers-task-validator.js +41 -0
- package/package.json +69 -69
|
@@ -58,15 +58,25 @@ async function applyMapping(projectRoot, mapping) {
|
|
|
58
58
|
if (typeof rendered === "string") {
|
|
59
59
|
return (await writeTextIfChanged(target, rendered)) ? [mapping.target] : [];
|
|
60
60
|
}
|
|
61
|
-
await fs.rm(target, { recursive: true, force: true });
|
|
62
61
|
await ensureDir(target);
|
|
63
62
|
const changed = [];
|
|
63
|
+
const expectedRelatives = new Set(rendered.map((item) => item.relative));
|
|
64
64
|
for (const item of rendered) {
|
|
65
65
|
const targetFile = path.join(target, item.relative);
|
|
66
66
|
if (await writeTextIfChanged(targetFile, item.content)) {
|
|
67
67
|
changed.push(`${mapping.target}/${item.relative}`);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
for (const targetFile of await listFiles(target)) {
|
|
71
|
+
if (path.basename(targetFile) === ".gitkeep") {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const relative = path.relative(target, targetFile);
|
|
75
|
+
if (!expectedRelatives.has(relative)) {
|
|
76
|
+
await fs.rm(targetFile, { force: true });
|
|
77
|
+
changed.push(`${mapping.target}/${relative}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
70
80
|
return changed;
|
|
71
81
|
}
|
|
72
82
|
async function renderMapping(projectRoot, mapping) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CommandRunRecord, ExecutionAttempt, RequiredCommandSpec, SuperpowersTaskState } from "./superpowers-task-state-schema.js";
|
|
2
|
+
export interface CommandRunCorrelationResult {
|
|
3
|
+
errors: string[];
|
|
4
|
+
invalidated_evidence_ids: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function validateRequiredCommandCorrelation(state: SuperpowersTaskState, attempt: ExecutionAttempt | undefined, expectedSpecs: RequiredCommandSpec[]): CommandRunCorrelationResult;
|
|
7
|
+
export declare function validateCommandRunsForSpec(state: SuperpowersTaskState, attempt: ExecutionAttempt | undefined, spec: RequiredCommandSpec): string[];
|
|
8
|
+
export declare function validateCommandRun(run: CommandRunRecord, attempt: ExecutionAttempt | undefined): string[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { isMachineVerifiableLayer } from "./superpowers-task-assertions.js";
|
|
2
|
+
import { normalizeProofLayerName } from "./superpowers-task-fields.js";
|
|
3
|
+
import { requiredCommandSpecsHash } from "./superpowers-task-command-specs.js";
|
|
4
|
+
export function validateRequiredCommandCorrelation(state, attempt, expectedSpecs) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
const invalidated = new Set();
|
|
7
|
+
const expectedByAc = new Map(expectedSpecs.map((spec) => [spec.ac_id, spec]));
|
|
8
|
+
for (const [acId, ac] of Object.entries(state.graph?.acceptance_criteria ?? {})) {
|
|
9
|
+
if (ac.machine_blocking !== true && ac.assertion_result_required !== true) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const expected = expectedByAc.get(acId);
|
|
13
|
+
const actual = (state.required_command_specs ?? []).find((spec) => spec.ac_id === acId);
|
|
14
|
+
if (!expected || !actual) {
|
|
15
|
+
errors.push(`${acId} missing required_command_spec`);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (actual.command_spec_id !== expected.command_spec_id) {
|
|
19
|
+
errors.push(`${acId} command_spec_id mismatch; required command specs must be recompiled from Acceptance Checklist`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (attempt) {
|
|
23
|
+
const specsHash = requiredCommandSpecsHash(state.required_command_specs ?? []);
|
|
24
|
+
if (attempt.required_command_specs_hash !== specsHash) {
|
|
25
|
+
errors.push("required_command_specs_hash mismatch for current attempt");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const spec of expectedSpecs) {
|
|
29
|
+
errors.push(...validateCommandRunsForSpec(state, attempt, spec));
|
|
30
|
+
}
|
|
31
|
+
for (const finding of findFailedCommandInvalidations(state)) {
|
|
32
|
+
invalidated.add(finding.evidence_id);
|
|
33
|
+
errors.push(`${finding.evidence_id} newer failed command invalidates older passed evidence for ${finding.ac_id}.${finding.proof_layer}: ${finding.command_run_id}`);
|
|
34
|
+
}
|
|
35
|
+
return { errors: unique(errors), invalidated_evidence_ids: [...invalidated] };
|
|
36
|
+
}
|
|
37
|
+
export function validateCommandRunsForSpec(state, attempt, spec) {
|
|
38
|
+
const errors = [];
|
|
39
|
+
for (const proofLayer of spec.proof_layers.filter((layer) => isMachineVerifiableLayer(`${spec.ac_id}.${layer}`))) {
|
|
40
|
+
const run = (state.command_runs ?? []).find((item) => item.task_attempt_id === state.current_attempt_id &&
|
|
41
|
+
item.command_spec_id === spec.command_spec_id &&
|
|
42
|
+
item.ac_id === spec.ac_id &&
|
|
43
|
+
normalizeProofLayerName(item.proof_layer) === normalizeProofLayerName(proofLayer));
|
|
44
|
+
if (!run) {
|
|
45
|
+
errors.push(`${spec.ac_id}.${proofLayer} missing current attempt command-run record for command_spec_id ${spec.command_spec_id}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
errors.push(...validateCommandRun(run, attempt));
|
|
49
|
+
}
|
|
50
|
+
return errors;
|
|
51
|
+
}
|
|
52
|
+
export function validateCommandRun(run, attempt) {
|
|
53
|
+
const errors = [];
|
|
54
|
+
if (attempt && run.task_attempt_id !== attempt.task_attempt_id) {
|
|
55
|
+
errors.push(`${run.command_run_id} stale command run from ${run.task_attempt_id}; expected ${attempt.task_attempt_id}`);
|
|
56
|
+
}
|
|
57
|
+
if (run.exit_code !== 0) {
|
|
58
|
+
errors.push(`${run.command_run_id} command_exit_code=${run.exit_code}; expected 0`);
|
|
59
|
+
}
|
|
60
|
+
if (!run.command_line.trim()) {
|
|
61
|
+
errors.push(`${run.command_run_id} missing command_line`);
|
|
62
|
+
}
|
|
63
|
+
if (!run.started_at) {
|
|
64
|
+
errors.push(`${run.command_run_id} missing started_at`);
|
|
65
|
+
}
|
|
66
|
+
if (!run.completed_at) {
|
|
67
|
+
errors.push(`${run.command_run_id} missing completed_at`);
|
|
68
|
+
}
|
|
69
|
+
if (!run.task_attempt_id) {
|
|
70
|
+
errors.push(`${run.command_run_id} missing attempt_id`);
|
|
71
|
+
}
|
|
72
|
+
return errors;
|
|
73
|
+
}
|
|
74
|
+
function findFailedCommandInvalidations(state) {
|
|
75
|
+
const evidence = state.evidence ?? [];
|
|
76
|
+
const failedRuns = (state.command_runs ?? []).filter((run) => run.task_attempt_id === state.current_attempt_id && Number(run.exit_code) !== 0);
|
|
77
|
+
return failedRuns.flatMap((run) => evidence
|
|
78
|
+
.filter((item) => evidenceTargetsRunLayer(item, run) && evidencePredatesRun(item, run))
|
|
79
|
+
.map((item) => ({
|
|
80
|
+
evidence_id: item.evidence_id,
|
|
81
|
+
command_run_id: run.command_run_id,
|
|
82
|
+
ac_id: run.ac_id,
|
|
83
|
+
proof_layer: run.proof_layer
|
|
84
|
+
})));
|
|
85
|
+
}
|
|
86
|
+
function evidenceTargetsRunLayer(evidence, run) {
|
|
87
|
+
const layerId = `${run.ac_id}.${normalizeProofLayerName(run.proof_layer)}`;
|
|
88
|
+
const targetLayers = (evidence.target_proof_layers ?? evidence.assertion_result?.target_proof_layers ?? []).map((item) => item.includes(".") ? item : `${run.ac_id}.${normalizeProofLayerName(item)}`);
|
|
89
|
+
return (evidence.task_attempt_id === run.task_attempt_id &&
|
|
90
|
+
(evidence.target_ac_ids ?? evidence.assertion_result?.target_ac_ids ?? []).includes(run.ac_id) &&
|
|
91
|
+
(targetLayers.includes(layerId) || evidence.proves.includes(layerId)));
|
|
92
|
+
}
|
|
93
|
+
function evidencePredatesRun(evidence, run) {
|
|
94
|
+
const evidenceTime = Date.parse(evidence.generated_at ?? evidence.freshness?.created_at ?? evidence.artifact_mtime ?? "");
|
|
95
|
+
const runTime = Date.parse(run.started_at || run.completed_at || run.ended_at || "");
|
|
96
|
+
if (Number.isNaN(evidenceTime) || Number.isNaN(runTime)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return evidenceTime <= runTime;
|
|
100
|
+
}
|
|
101
|
+
function unique(values) {
|
|
102
|
+
return [...new Set(values.filter(Boolean))];
|
|
103
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type SuperpowersTaskState } from "./superpowers-task-state-schema.js";
|
|
2
|
+
export type CompletionOutputStatus = "accept" | "reject" | "blocked";
|
|
3
|
+
export interface CompletionOutputContract {
|
|
4
|
+
product_goal_complete: boolean;
|
|
5
|
+
acceptance_target_status: string;
|
|
6
|
+
completion_output_status: CompletionOutputStatus;
|
|
7
|
+
final_answer_allowed: boolean;
|
|
8
|
+
required_user_visible_status: "accepted" | "rejected" | "blocked";
|
|
9
|
+
final_answer: CompletionOutputStatus;
|
|
10
|
+
exit_code: 0 | 1 | 2;
|
|
11
|
+
blocked_reasons: string[];
|
|
12
|
+
rejection_reasons: string[];
|
|
13
|
+
audit_task_complete: boolean;
|
|
14
|
+
final_gate_ran: boolean;
|
|
15
|
+
generated_output_mismatch: boolean;
|
|
16
|
+
blocker_triage?: FinalGateBlockerTriage;
|
|
17
|
+
candidate_state?: FinalGateCandidateState;
|
|
18
|
+
}
|
|
19
|
+
export interface CompletionOutputResolveInput {
|
|
20
|
+
final_gate_ran?: boolean;
|
|
21
|
+
product_goal_complete?: boolean;
|
|
22
|
+
acceptance_target_status?: string;
|
|
23
|
+
audit_task_complete?: boolean;
|
|
24
|
+
validator_errors?: string[];
|
|
25
|
+
acceptance_validator_errors?: string[];
|
|
26
|
+
blocked_reasons?: string[];
|
|
27
|
+
rejection_reasons?: string[];
|
|
28
|
+
generated_output_mismatch?: boolean;
|
|
29
|
+
required_command_not_run?: boolean;
|
|
30
|
+
environment_unknown?: boolean;
|
|
31
|
+
state_unreadable?: boolean;
|
|
32
|
+
source_unreadable?: boolean;
|
|
33
|
+
kernel_invalid?: boolean;
|
|
34
|
+
required_validator_unavailable?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface CompletionOutputSurface {
|
|
37
|
+
surface: string;
|
|
38
|
+
text: string;
|
|
39
|
+
}
|
|
40
|
+
export type CompletionOutputSurfaceType = "user_visible_final_summary" | "final_card" | "agent_final_answer" | "generated_summary" | "machine_readable_json_status" | "derived_diagnostic_json" | "matrix_diagnostic" | "verdict_diagnostic" | "workflow_protocol_text" | "execution_binding_text" | "rule_explanation_text" | "local_audit_diagnostic" | "unknown";
|
|
41
|
+
export type CompletionPhraseClassification = "true_false_completion_claim" | "allowed_protocol_reserved_word" | "allowed_machine_status_field" | "allowed_diagnostic_status" | "allowed_rule_explanation";
|
|
42
|
+
export interface CompletionPhraseFinding {
|
|
43
|
+
surface: string;
|
|
44
|
+
surface_type?: CompletionOutputSurfaceType;
|
|
45
|
+
classification?: CompletionPhraseClassification;
|
|
46
|
+
phrase: string;
|
|
47
|
+
line: number;
|
|
48
|
+
text: string;
|
|
49
|
+
self_recoverable?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface FinalGateCandidateState {
|
|
52
|
+
final_gate_ran: boolean;
|
|
53
|
+
product_goal_complete: boolean;
|
|
54
|
+
acceptance_target_status: string;
|
|
55
|
+
completion_output_status: CompletionOutputStatus;
|
|
56
|
+
generated_output_mismatch: boolean;
|
|
57
|
+
source: "trusted_evidence_kernel";
|
|
58
|
+
}
|
|
59
|
+
export type FinalGateBlockerCategory = "none" | "product_evidence_failed" | "missing_current_evidence" | "stale_or_contradictory_evidence" | "generated_output_mismatch" | "self_recoverable_generated_output_mismatch" | "transient_state_bookkeeping" | "environment_blocked" | "contract_blocked" | "harness_drift_blocked";
|
|
60
|
+
export interface FinalGateBlockerTriage {
|
|
61
|
+
category: FinalGateBlockerCategory;
|
|
62
|
+
self_recoverable: boolean;
|
|
63
|
+
recovery_attempted: boolean;
|
|
64
|
+
recovery_action: string;
|
|
65
|
+
next_action: string;
|
|
66
|
+
details: string[];
|
|
67
|
+
blocker_count: number;
|
|
68
|
+
}
|
|
69
|
+
export interface FinalGateBlockerTriageInput {
|
|
70
|
+
errors: string[];
|
|
71
|
+
output_findings: CompletionPhraseFinding[];
|
|
72
|
+
previous_transient_findings?: string[];
|
|
73
|
+
candidate_state?: FinalGateCandidateState;
|
|
74
|
+
recovery_attempted?: boolean;
|
|
75
|
+
recovery_action?: string;
|
|
76
|
+
}
|
|
77
|
+
export declare function resolveCompletionOutputStatus(input: CompletionOutputResolveInput): CompletionOutputContract;
|
|
78
|
+
export declare function completionOutputContractFromState(state: SuperpowersTaskState): CompletionOutputContract;
|
|
79
|
+
export declare function applyCompletionOutputContract(state: SuperpowersTaskState, contract: CompletionOutputContract): void;
|
|
80
|
+
export declare function scanFalseCompletionPhrases(input: {
|
|
81
|
+
completion_output_status: CompletionOutputStatus;
|
|
82
|
+
surfaces: CompletionOutputSurface[] | string;
|
|
83
|
+
}): CompletionPhraseFinding[];
|
|
84
|
+
export declare function scanFalseCompletionPhrasesDetailed(input: {
|
|
85
|
+
completion_output_status: CompletionOutputStatus;
|
|
86
|
+
surfaces: CompletionOutputSurface[] | string;
|
|
87
|
+
}): CompletionPhraseFinding[];
|
|
88
|
+
export declare function scanGeneratedCompletionOutputSurfaces(workdir: string, contract: CompletionOutputContract): Promise<CompletionPhraseFinding[]>;
|
|
89
|
+
export declare function scanGeneratedCompletionOutputSurfacesDetailed(workdir: string, contract: CompletionOutputContract): Promise<CompletionPhraseFinding[]>;
|
|
90
|
+
export declare function completionPhraseFindingMessages(findings: CompletionPhraseFinding[]): string[];
|
|
91
|
+
export declare function triageFinalGateBlockers(input: FinalGateBlockerTriageInput): FinalGateBlockerTriage;
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathExists, readText } from "./fs.js";
|
|
3
|
+
import { asStringArray, isRecord } from "./superpowers-task-state-schema.js";
|
|
4
|
+
const BLOCKING_TARGET_STATUSES = new Set(["not_run", "blocked", "under_specified", "unknown"]);
|
|
5
|
+
const REJECTING_TARGET_STATUSES = new Set(["partial", "failed", "rejected", "invalidated", "not_accepted"]);
|
|
6
|
+
const GENERATED_COMPLETION_SURFACES = [
|
|
7
|
+
"derived/final-summary.md",
|
|
8
|
+
"derived/final-card.md",
|
|
9
|
+
"derived/final-acceptance-verdict.json",
|
|
10
|
+
"derived/final-acceptance-verdict.md",
|
|
11
|
+
"derived/plan-conformance-matrix.json",
|
|
12
|
+
"derived/plan-conformance-matrix.md",
|
|
13
|
+
"derived/local-audit.md",
|
|
14
|
+
"goal-objective.txt",
|
|
15
|
+
"execution-binding.md",
|
|
16
|
+
"workflow-protocol.md"
|
|
17
|
+
];
|
|
18
|
+
const FORBIDDEN_PHRASES = [
|
|
19
|
+
{ phrase: "update_goal(status=\"complete\")", pattern: /update_goal\s*\(\s*status\s*=\s*["']complete["']\s*\)/i },
|
|
20
|
+
{ phrase: "goal achieved", pattern: /\bgoal achieved\b/i },
|
|
21
|
+
{ phrase: "ready to merge", pattern: /\bready to merge\b/i },
|
|
22
|
+
{ phrase: "implementation complete", pattern: /\bimplementation complete\b/i },
|
|
23
|
+
{ phrase: "all passed", pattern: /\ball (?:acs?|acceptance criteria|checks|tests)?\s*passed\b/i },
|
|
24
|
+
{ phrase: "product complete", pattern: /\bproduct complete\b/i },
|
|
25
|
+
{ phrase: "completed", pattern: /\bcompleted\b/i },
|
|
26
|
+
{ phrase: "complete", pattern: /\bcomplete\b/i },
|
|
27
|
+
{ phrase: "accepted", pattern: /\baccepted\b/i },
|
|
28
|
+
{ phrase: "accept", pattern: /\baccept\b/i },
|
|
29
|
+
{ phrase: "done", pattern: /\bdone\b/i },
|
|
30
|
+
{ phrase: "success", pattern: /\bsuccess(?:ful)?\b/i },
|
|
31
|
+
{ phrase: "任务完成", pattern: /任务完成/ },
|
|
32
|
+
{ phrase: "已完成", pattern: /已完成/ },
|
|
33
|
+
{ phrase: "验收通过", pattern: /验收通过/ },
|
|
34
|
+
{ phrase: "目标完成", pattern: /目标完成/ },
|
|
35
|
+
{ phrase: "全部通过", pattern: /全部通过/ },
|
|
36
|
+
{ phrase: "可以合并", pattern: /可以合并/ }
|
|
37
|
+
];
|
|
38
|
+
export function resolveCompletionOutputStatus(input) {
|
|
39
|
+
const finalGateRan = input.final_gate_ran === true;
|
|
40
|
+
const productGoalComplete = input.product_goal_complete === true;
|
|
41
|
+
const acceptanceTargetStatus = normalizeAcceptanceTargetStatus(input.acceptance_target_status);
|
|
42
|
+
const validatorErrors = [...(input.validator_errors ?? []), ...(input.acceptance_validator_errors ?? [])].filter(Boolean);
|
|
43
|
+
const blockedReasons = [...(input.blocked_reasons ?? [])];
|
|
44
|
+
const rejectionReasons = [...(input.rejection_reasons ?? [])];
|
|
45
|
+
if (!finalGateRan) {
|
|
46
|
+
blockedReasons.push("final_gate_not_run");
|
|
47
|
+
}
|
|
48
|
+
if (input.required_command_not_run === true) {
|
|
49
|
+
blockedReasons.push("required_command_not_run");
|
|
50
|
+
}
|
|
51
|
+
if (input.environment_unknown === true) {
|
|
52
|
+
blockedReasons.push("environment_unknown");
|
|
53
|
+
}
|
|
54
|
+
if (input.state_unreadable === true) {
|
|
55
|
+
blockedReasons.push("state_unreadable");
|
|
56
|
+
}
|
|
57
|
+
if (input.source_unreadable === true) {
|
|
58
|
+
blockedReasons.push("source_unreadable");
|
|
59
|
+
}
|
|
60
|
+
if (input.kernel_invalid === true) {
|
|
61
|
+
blockedReasons.push("kernel_invalid");
|
|
62
|
+
}
|
|
63
|
+
if (input.required_validator_unavailable === true) {
|
|
64
|
+
blockedReasons.push("required_validator_unavailable");
|
|
65
|
+
}
|
|
66
|
+
if (input.generated_output_mismatch === true) {
|
|
67
|
+
blockedReasons.push("generated_output_mismatch");
|
|
68
|
+
}
|
|
69
|
+
if (BLOCKING_TARGET_STATUSES.has(acceptanceTargetStatus)) {
|
|
70
|
+
blockedReasons.push(`acceptance_target_status=${acceptanceTargetStatus}`);
|
|
71
|
+
}
|
|
72
|
+
if (REJECTING_TARGET_STATUSES.has(acceptanceTargetStatus)) {
|
|
73
|
+
rejectionReasons.push(`acceptance_target_status=${acceptanceTargetStatus}`);
|
|
74
|
+
}
|
|
75
|
+
if (finalGateRan && !productGoalComplete && !BLOCKING_TARGET_STATUSES.has(acceptanceTargetStatus)) {
|
|
76
|
+
rejectionReasons.push("product_goal_complete=false");
|
|
77
|
+
}
|
|
78
|
+
if (validatorErrors.length > 0) {
|
|
79
|
+
rejectionReasons.push(...validatorErrors);
|
|
80
|
+
}
|
|
81
|
+
const accept = finalGateRan &&
|
|
82
|
+
productGoalComplete &&
|
|
83
|
+
isAcceptedStatus(acceptanceTargetStatus) &&
|
|
84
|
+
validatorErrors.length === 0 &&
|
|
85
|
+
blockedReasons.length === 0 &&
|
|
86
|
+
rejectionReasons.length === 0;
|
|
87
|
+
const completionOutputStatus = accept ? "accept" : blockedReasons.length > 0 ? "blocked" : "reject";
|
|
88
|
+
const exitCode = completionOutputStatus === "accept" ? 0 : completionOutputStatus === "reject" ? 1 : 2;
|
|
89
|
+
return {
|
|
90
|
+
product_goal_complete: productGoalComplete && accept,
|
|
91
|
+
acceptance_target_status: acceptanceTargetStatus,
|
|
92
|
+
completion_output_status: completionOutputStatus,
|
|
93
|
+
final_answer_allowed: completionOutputStatus === "accept",
|
|
94
|
+
required_user_visible_status: completionOutputStatus === "accept" ? "accepted" : completionOutputStatus === "reject" ? "rejected" : "blocked",
|
|
95
|
+
final_answer: completionOutputStatus,
|
|
96
|
+
exit_code: exitCode,
|
|
97
|
+
blocked_reasons: unique(blockedReasons),
|
|
98
|
+
rejection_reasons: unique(rejectionReasons),
|
|
99
|
+
audit_task_complete: input.audit_task_complete === true,
|
|
100
|
+
final_gate_ran: finalGateRan,
|
|
101
|
+
generated_output_mismatch: input.generated_output_mismatch === true
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function completionOutputContractFromState(state) {
|
|
105
|
+
const finalRecord = state.final;
|
|
106
|
+
const gate = isRecord(state.gates?.final_gate) ? state.gates.final_gate : {};
|
|
107
|
+
const hasStoredOutput = typeof finalRecord.completion_output_status === "string" || typeof gate.completion_output_status === "string";
|
|
108
|
+
const contract = resolveCompletionOutputStatus({
|
|
109
|
+
final_gate_ran: hasStoredOutput || isRecord(state.gates?.final_gate),
|
|
110
|
+
product_goal_complete: state.final.product_goal_complete,
|
|
111
|
+
acceptance_target_status: state.final.acceptance_target_status,
|
|
112
|
+
audit_task_complete: state.final.audit_task_complete,
|
|
113
|
+
blocked_reasons: asStringArray(finalRecord.blocked_reasons ?? gate.blocked_reasons),
|
|
114
|
+
rejection_reasons: asStringArray(finalRecord.rejection_reasons ?? gate.rejection_reasons),
|
|
115
|
+
generated_output_mismatch: finalRecord.generated_output_mismatch === true || gate.generated_output_mismatch === true
|
|
116
|
+
});
|
|
117
|
+
const triage = isRecord(finalRecord.blocker_triage)
|
|
118
|
+
? finalRecord.blocker_triage
|
|
119
|
+
: isRecord(gate.blocker_triage)
|
|
120
|
+
? gate.blocker_triage
|
|
121
|
+
: undefined;
|
|
122
|
+
const candidate = isRecord(finalRecord.candidate_state)
|
|
123
|
+
? finalRecord.candidate_state
|
|
124
|
+
: isRecord(gate.candidate_state)
|
|
125
|
+
? gate.candidate_state
|
|
126
|
+
: undefined;
|
|
127
|
+
if (triage) {
|
|
128
|
+
contract.blocker_triage = triage;
|
|
129
|
+
}
|
|
130
|
+
if (candidate) {
|
|
131
|
+
contract.candidate_state = candidate;
|
|
132
|
+
}
|
|
133
|
+
return contract;
|
|
134
|
+
}
|
|
135
|
+
export function applyCompletionOutputContract(state, contract) {
|
|
136
|
+
const finalRecord = state.final;
|
|
137
|
+
const metaRecord = state.meta;
|
|
138
|
+
finalRecord.product_goal_complete = contract.product_goal_complete;
|
|
139
|
+
state.meta.product_goal_complete = contract.product_goal_complete;
|
|
140
|
+
finalRecord.acceptance_target_status = contract.acceptance_target_status;
|
|
141
|
+
state.meta.acceptance_target_status = contract.acceptance_target_status;
|
|
142
|
+
finalRecord.audit_task_complete = contract.audit_task_complete;
|
|
143
|
+
state.meta.audit_task_complete = contract.audit_task_complete;
|
|
144
|
+
finalRecord.completion_output_status = contract.completion_output_status;
|
|
145
|
+
finalRecord.final_answer_allowed = contract.final_answer_allowed;
|
|
146
|
+
finalRecord.required_user_visible_status = contract.required_user_visible_status;
|
|
147
|
+
finalRecord.final_answer = contract.final_answer;
|
|
148
|
+
finalRecord.exit_code = contract.exit_code;
|
|
149
|
+
finalRecord.blocked_reasons = contract.blocked_reasons;
|
|
150
|
+
finalRecord.rejection_reasons = contract.rejection_reasons;
|
|
151
|
+
finalRecord.generated_output_mismatch = contract.generated_output_mismatch;
|
|
152
|
+
if (contract.blocker_triage) {
|
|
153
|
+
finalRecord.blocker_triage = contract.blocker_triage;
|
|
154
|
+
}
|
|
155
|
+
if (contract.candidate_state) {
|
|
156
|
+
finalRecord.candidate_state = contract.candidate_state;
|
|
157
|
+
}
|
|
158
|
+
metaRecord.completion_output_status = contract.completion_output_status;
|
|
159
|
+
}
|
|
160
|
+
export function scanFalseCompletionPhrases(input) {
|
|
161
|
+
return scanFalseCompletionPhrasesDetailed(input);
|
|
162
|
+
}
|
|
163
|
+
export function scanFalseCompletionPhrasesDetailed(input) {
|
|
164
|
+
if (input.completion_output_status === "accept") {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const surfaces = typeof input.surfaces === "string" ? [{ surface: "inline", text: input.surfaces }] : input.surfaces;
|
|
168
|
+
const findings = [];
|
|
169
|
+
for (const surface of surfaces) {
|
|
170
|
+
const lines = surface.text.split(/\r?\n/);
|
|
171
|
+
for (const [index, line] of lines.entries()) {
|
|
172
|
+
const allowed = classifyAllowedCompletionLine(surface.surface, line);
|
|
173
|
+
if (allowed) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
for (const item of FORBIDDEN_PHRASES) {
|
|
177
|
+
if (item.pattern.test(line)) {
|
|
178
|
+
findings.push({
|
|
179
|
+
surface: surface.surface,
|
|
180
|
+
surface_type: classifySurfaceLine(surface.surface, line),
|
|
181
|
+
classification: "true_false_completion_claim",
|
|
182
|
+
phrase: item.phrase,
|
|
183
|
+
line: index + 1,
|
|
184
|
+
text: line.trim(),
|
|
185
|
+
self_recoverable: isSelfRecoverableSurface(surface.surface)
|
|
186
|
+
});
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return findings;
|
|
193
|
+
}
|
|
194
|
+
export async function scanGeneratedCompletionOutputSurfaces(workdir, contract) {
|
|
195
|
+
return scanGeneratedCompletionOutputSurfacesDetailed(workdir, contract);
|
|
196
|
+
}
|
|
197
|
+
export async function scanGeneratedCompletionOutputSurfacesDetailed(workdir, contract) {
|
|
198
|
+
const surfaces = [];
|
|
199
|
+
for (const relative of GENERATED_COMPLETION_SURFACES) {
|
|
200
|
+
const file = path.join(workdir, ...relative.split("/"));
|
|
201
|
+
if (await pathExists(file)) {
|
|
202
|
+
surfaces.push({ surface: relative, text: await readText(file) });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return scanFalseCompletionPhrasesDetailed({ completion_output_status: contract.completion_output_status, surfaces });
|
|
206
|
+
}
|
|
207
|
+
export function completionPhraseFindingMessages(findings) {
|
|
208
|
+
return findings.map((finding) => {
|
|
209
|
+
const kind = finding.classification ? ` ${finding.classification}` : "";
|
|
210
|
+
return `false completion phrase${kind} in ${finding.surface}:${finding.line}: ${finding.phrase}`;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
export function triageFinalGateBlockers(input) {
|
|
214
|
+
const errors = unique(input.errors);
|
|
215
|
+
const findings = input.output_findings ?? [];
|
|
216
|
+
const previous = unique(input.previous_transient_findings ?? []);
|
|
217
|
+
const details = unique([...errors, ...findings.map((finding) => `${finding.surface}:${finding.line}:${finding.phrase}`), ...previous]);
|
|
218
|
+
const recoveryAttempted = input.recovery_attempted === true;
|
|
219
|
+
const recoveryAction = input.recovery_action ?? "";
|
|
220
|
+
if (findings.length > 0) {
|
|
221
|
+
const selfRecoverable = findings.every((finding) => finding.self_recoverable === true);
|
|
222
|
+
return {
|
|
223
|
+
category: selfRecoverable ? "self_recoverable_generated_output_mismatch" : "generated_output_mismatch",
|
|
224
|
+
self_recoverable: selfRecoverable,
|
|
225
|
+
recovery_attempted: recoveryAttempted,
|
|
226
|
+
recovery_action: recoveryAction,
|
|
227
|
+
next_action: selfRecoverable
|
|
228
|
+
? "regenerate derived generated-output surfaces and rerun final-gate once"
|
|
229
|
+
: "remove or regenerate the user-visible false completion wording before final-gate can accept",
|
|
230
|
+
details,
|
|
231
|
+
blocker_count: findings.length
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const text = errors.join("\n");
|
|
235
|
+
if (/harness_drift|protected_baseline|product task modified|harness task missing/i.test(text)) {
|
|
236
|
+
return blocker("harness_drift_blocked", false, recoveryAttempted, recoveryAction, "split harness changes into a harness_task with adversarial fixtures before proving product completion", details);
|
|
237
|
+
}
|
|
238
|
+
if (/source file is missing|source_unreadable|scope_conflict_requires_decision|three[- ]input|Product \/ Plan \/ Checklist|Context Delta coverage is unresolved/i.test(text)) {
|
|
239
|
+
return blocker("contract_blocked", false, recoveryAttempted, recoveryAction, "clarify or restore the source contract before rerunning final-gate", details);
|
|
240
|
+
}
|
|
241
|
+
if (/required command not_run|command not_run|browser unavailable|playwright unavailable|dependency unavailable|environment_unknown|required_validator_unavailable|permission|MFA/i.test(text)) {
|
|
242
|
+
return blocker("environment_blocked", false, recoveryAttempted, recoveryAction, "restore the unavailable command, browser, dependency or permission and rerun final-gate", details);
|
|
243
|
+
}
|
|
244
|
+
if (/stale evidence|negative evidence|current contradiction|source hash mismatch|dirty worktree|failed_test_result_artifact|owner_dom_forbidden_state|playwright_last_run_failed/i.test(text)) {
|
|
245
|
+
return blocker("stale_or_contradictory_evidence", false, recoveryAttempted, recoveryAction, "replace stale or contradictory evidence with fresh current-attempt evidence", details);
|
|
246
|
+
}
|
|
247
|
+
if (/missing current|missing assertion result|not machine-backed|missing required proof layer|proof layer .*missing|requires all required plan items|incomplete|no evidence_ids|unknown evidence_id/i.test(text)) {
|
|
248
|
+
return blocker("missing_current_evidence", false, recoveryAttempted, recoveryAction, "add fresh current-attempt evidence for the missing AC, PI or proof layer", details);
|
|
249
|
+
}
|
|
250
|
+
if (previous.length > 0 && errors.length === 0) {
|
|
251
|
+
return blocker("transient_state_bookkeeping", true, recoveryAttempted, recoveryAction || "cleared previous transient bookkeeping before current candidate scan", "cleared previous transient bookkeeping; no user action required", details);
|
|
252
|
+
}
|
|
253
|
+
if (errors.length > 0) {
|
|
254
|
+
return blocker("product_evidence_failed", false, recoveryAttempted, recoveryAction, "fix the current product evidence failure and rerun final-gate", details);
|
|
255
|
+
}
|
|
256
|
+
return blocker("none", false, recoveryAttempted, recoveryAction, "no blocker remains", details);
|
|
257
|
+
}
|
|
258
|
+
function normalizeAcceptanceTargetStatus(value) {
|
|
259
|
+
const normalized = String(value ?? "not_run").trim().toLowerCase();
|
|
260
|
+
if (normalized === "accepted") {
|
|
261
|
+
return "complete";
|
|
262
|
+
}
|
|
263
|
+
if (normalized === "not accepted") {
|
|
264
|
+
return "not_accepted";
|
|
265
|
+
}
|
|
266
|
+
return normalized || "not_run";
|
|
267
|
+
}
|
|
268
|
+
function isAcceptedStatus(value) {
|
|
269
|
+
return value === "complete" || value === "accepted";
|
|
270
|
+
}
|
|
271
|
+
function classifyAllowedCompletionLine(surface, line) {
|
|
272
|
+
const text = line.trim();
|
|
273
|
+
if (!text) {
|
|
274
|
+
return "allowed_rule_explanation";
|
|
275
|
+
}
|
|
276
|
+
if (/Audit workflow completed; acceptance target not complete\./i.test(text)) {
|
|
277
|
+
return "allowed_rule_explanation";
|
|
278
|
+
}
|
|
279
|
+
if (/^Product goal complete:\s*false$/i.test(text)) {
|
|
280
|
+
return "allowed_diagnostic_status";
|
|
281
|
+
}
|
|
282
|
+
if (/^(?:complete|partial|acceptance_required|missing_layer)_count:\s*\d+$/i.test(text)) {
|
|
283
|
+
return "allowed_diagnostic_status";
|
|
284
|
+
}
|
|
285
|
+
if (/^-\s+[A-Z]+-\d+:\s*(?:complete|accepted|accept)\s*$/i.test(text)) {
|
|
286
|
+
return "allowed_diagnostic_status";
|
|
287
|
+
}
|
|
288
|
+
const jsonField = /^"([^"]+)"\s*:\s*/.exec(text);
|
|
289
|
+
if (jsonField && !/^(final_answer|final_conclusion|conclusion|summary|message|required_user_visible_status)$/i.test(jsonField[1])) {
|
|
290
|
+
return "allowed_machine_status_field";
|
|
291
|
+
}
|
|
292
|
+
if (/\b(product_goal_complete|completion_output_status|acceptance_target_status|audit_task_complete)\b/i.test(text)) {
|
|
293
|
+
return "allowed_diagnostic_status";
|
|
294
|
+
}
|
|
295
|
+
if (isDiagnosticSurface(surface) && /["']?(overall_)?status["']?\s*:\s*["']?(complete|accepted|accept)["']?/i.test(text)) {
|
|
296
|
+
return "allowed_diagnostic_status";
|
|
297
|
+
}
|
|
298
|
+
if (/diagnostic|row-level|row status|not[_ -]?in[_ -]?scope/i.test(text)) {
|
|
299
|
+
return "allowed_diagnostic_status";
|
|
300
|
+
}
|
|
301
|
+
if (/\b(do not|must not|cannot|never|unless|only when|forbid|forbidden|invalid|false[- ]completion|does not mean|cannot authorize|cannot imply)\b/i.test(text)) {
|
|
302
|
+
return "allowed_rule_explanation";
|
|
303
|
+
}
|
|
304
|
+
if (/\bnot\s+(?:complete|completed|accepted|accept|done|successful)\b/i.test(text)) {
|
|
305
|
+
return "allowed_rule_explanation";
|
|
306
|
+
}
|
|
307
|
+
if (/不得|不能|禁止|仅当|不是|不等于/.test(text)) {
|
|
308
|
+
return "allowed_rule_explanation";
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
function classifySurfaceLine(surface, line) {
|
|
313
|
+
const normalized = surface.split(path.sep).join("/");
|
|
314
|
+
if (/^Final answer:|Goal achieved|ready to merge|implementation complete/i.test(line)) {
|
|
315
|
+
return "agent_final_answer";
|
|
316
|
+
}
|
|
317
|
+
if (normalized.endsWith("derived/final-summary.md")) {
|
|
318
|
+
return "user_visible_final_summary";
|
|
319
|
+
}
|
|
320
|
+
if (normalized.endsWith("derived/final-card.md")) {
|
|
321
|
+
return "final_card";
|
|
322
|
+
}
|
|
323
|
+
if (normalized.endsWith("goal-objective.txt")) {
|
|
324
|
+
return "agent_final_answer";
|
|
325
|
+
}
|
|
326
|
+
if (normalized.endsWith("execution-binding.md")) {
|
|
327
|
+
return "execution_binding_text";
|
|
328
|
+
}
|
|
329
|
+
if (normalized.endsWith("workflow-protocol.md")) {
|
|
330
|
+
return "workflow_protocol_text";
|
|
331
|
+
}
|
|
332
|
+
if (/final-acceptance-verdict\.json$/.test(normalized)) {
|
|
333
|
+
return "verdict_diagnostic";
|
|
334
|
+
}
|
|
335
|
+
if (/plan-conformance-matrix\.json$/.test(normalized)) {
|
|
336
|
+
return "matrix_diagnostic";
|
|
337
|
+
}
|
|
338
|
+
if (/\.json$/.test(normalized)) {
|
|
339
|
+
return "machine_readable_json_status";
|
|
340
|
+
}
|
|
341
|
+
if (/local-audit/.test(normalized)) {
|
|
342
|
+
return "local_audit_diagnostic";
|
|
343
|
+
}
|
|
344
|
+
if (/derived\//.test(normalized)) {
|
|
345
|
+
return "generated_summary";
|
|
346
|
+
}
|
|
347
|
+
return "unknown";
|
|
348
|
+
}
|
|
349
|
+
function isSelfRecoverableSurface(surface) {
|
|
350
|
+
const normalized = surface.split(path.sep).join("/");
|
|
351
|
+
return /^derived\/(?:final-summary|final-card|final-acceptance-verdict|plan-conformance-matrix|local-audit)\.(?:md|json)$/.test(normalized);
|
|
352
|
+
}
|
|
353
|
+
function isDiagnosticSurface(surface) {
|
|
354
|
+
const type = classifySurfaceLine(surface, "");
|
|
355
|
+
return (type === "machine_readable_json_status" ||
|
|
356
|
+
type === "derived_diagnostic_json" ||
|
|
357
|
+
type === "matrix_diagnostic" ||
|
|
358
|
+
type === "verdict_diagnostic" ||
|
|
359
|
+
type === "local_audit_diagnostic");
|
|
360
|
+
}
|
|
361
|
+
function blocker(category, selfRecoverable, recoveryAttempted, recoveryAction, nextAction, details) {
|
|
362
|
+
return {
|
|
363
|
+
category,
|
|
364
|
+
self_recoverable: selfRecoverable,
|
|
365
|
+
recovery_attempted: recoveryAttempted,
|
|
366
|
+
recovery_action: recoveryAction,
|
|
367
|
+
next_action: nextAction,
|
|
368
|
+
details,
|
|
369
|
+
blocker_count: category === "none" ? 0 : Math.max(1, details.length)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function unique(values) {
|
|
373
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
374
|
+
}
|
|
@@ -15,11 +15,15 @@ export function evaluateCurrentAttemptEvidence(state, evidence, layerId) {
|
|
|
15
15
|
failures.push(`${label} stale evidence from old attempt ${evidence.task_attempt_id || "(missing)"}; expected current attempt ${state.current_attempt_id}`);
|
|
16
16
|
}
|
|
17
17
|
for (const field of [
|
|
18
|
+
"generated_at",
|
|
18
19
|
"source_bundle_hash",
|
|
19
20
|
"product_source_hash",
|
|
20
21
|
"technical_plan_hash",
|
|
21
22
|
"acceptance_checklist_hash",
|
|
22
23
|
"git_head",
|
|
24
|
+
"git_status_short",
|
|
25
|
+
"tracked_diff_hash",
|
|
26
|
+
"relevant_untracked_hash",
|
|
23
27
|
"worktree_fingerprint",
|
|
24
28
|
"command_spec_id",
|
|
25
29
|
"command_run_id",
|
|
@@ -48,9 +52,24 @@ export function evaluateCurrentAttemptEvidence(state, evidence, layerId) {
|
|
|
48
52
|
if (attempt && evidence.git_head && evidence.git_head !== attempt.git_head) {
|
|
49
53
|
failures.push(`${label} stale evidence git_head mismatch for current attempt`);
|
|
50
54
|
}
|
|
55
|
+
if (attempt && evidence.git_status_short !== undefined && evidence.git_status_short !== attempt.git_status_short) {
|
|
56
|
+
failures.push(`${label} stale evidence git_status_short mismatch for current attempt`);
|
|
57
|
+
}
|
|
58
|
+
if (attempt && evidence.tracked_diff_hash !== undefined && evidence.tracked_diff_hash !== attempt.tracked_diff_hash) {
|
|
59
|
+
failures.push(`${label} stale evidence tracked_diff_hash mismatch for current attempt`);
|
|
60
|
+
}
|
|
61
|
+
if (attempt && evidence.relevant_untracked_hash !== undefined && evidence.relevant_untracked_hash !== attempt.relevant_untracked_hash) {
|
|
62
|
+
failures.push(`${label} stale evidence relevant_untracked_hash mismatch for current attempt`);
|
|
63
|
+
}
|
|
51
64
|
if (attempt && evidence.worktree_fingerprint && evidence.worktree_fingerprint !== attempt.worktree_fingerprint) {
|
|
52
65
|
failures.push(`${label} stale evidence worktree_fingerprint mismatch for current attempt`);
|
|
53
66
|
}
|
|
67
|
+
if (attempt && (attempt.git_status_short || attempt.relevant_untracked_hash !== "none") && evidence.covers_dirty_worktree !== true) {
|
|
68
|
+
failures.push(`${label} dirty worktree evidence must set covers_dirty_worktree=true for current attempt`);
|
|
69
|
+
}
|
|
70
|
+
if (attempt && evidence.generated_at && Date.parse(evidence.generated_at) < Date.parse(attempt.started_at)) {
|
|
71
|
+
failures.push(`${label} stale evidence generated_at predates current attempt`);
|
|
72
|
+
}
|
|
54
73
|
if (attempt && evidence.artifact_mtime && Date.parse(evidence.artifact_mtime) < Date.parse(attempt.started_at)) {
|
|
55
74
|
failures.push(`${label} stale evidence artifact_mtime predates current attempt`);
|
|
56
75
|
}
|
|
@@ -65,6 +84,9 @@ export function evaluateCurrentAttemptEvidence(state, evidence, layerId) {
|
|
|
65
84
|
if (commandRun.exit_code !== 0) {
|
|
66
85
|
failures.push(`${label} command run ${commandRun.command_run_id} exit_code=${commandRun.exit_code}; expected 0`);
|
|
67
86
|
}
|
|
87
|
+
if (!commandRun.completed_at) {
|
|
88
|
+
failures.push(`${label} command run ${commandRun.command_run_id} missing completed_at`);
|
|
89
|
+
}
|
|
68
90
|
if (commandRun.command_spec_id !== evidence.command_spec_id) {
|
|
69
91
|
failures.push(`${label} command_spec_id mismatch between evidence and command run`);
|
|
70
92
|
}
|