@khoavu882/doflow 2.0.0-beta.4 → 2.0.0-beta.5
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/core/.antigravity-plugin/plugin.json +0 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +24 -1
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +1 -1
- package/src/adapters/antigravity/index.js +83 -47
- package/src/adapters/claude/index.js +15 -1
- package/src/adapters/codex/hooks.js +40 -5
- package/src/adapters/codex/index.js +52 -9
- package/src/adapters/copy-tree.js +4 -4
- package/src/adapters/gemini/hooks.js +57 -16
- package/src/adapters/gemini/index.js +57 -18
- package/src/adapters/kiro/index.js +15 -1
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/index.js +34 -9
- package/src/cli/commands/install.js +1 -1
- package/src/cli/commands/update.js +1 -1
- package/src/cli/index.js +17 -5
- package/src/cli/runtime-commands.js +51 -12
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/index.js +28 -14
- package/src/registry/index.js +136 -7
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/claims.js +14 -5
- package/src/runtime/cli-result.js +11 -4
- package/src/runtime/cli.js +35 -268
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1029 -244
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/evidence-ledger.js +7 -0
- package/src/runtime/freshness.js +44 -7
- package/src/runtime/outcome.js +1 -1
- package/src/runtime/readiness.js +83 -13
- package/src/runtime/retrieval-plan.js +422 -73
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/artifacts.js +3 -0
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/task-attempts.js +1 -1
- package/src/runtime/task-scope.js +28 -3
- package/src/runtime/task-state.js +72 -7
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/** The names a retrieval score arrives under, in camelCase (`--relevance-score` normalises here).
|
|
6
|
+
* Exported because `bin/doflow.js` refuses the same names on the command line, and because
|
|
7
|
+
* `retrieval-plan.ts` and `outcome.ts` refuse them on the option object a declared plan and a
|
|
8
|
+
* terminal record are built from: one definition, three input surfaces (argv, a `--batch` JSON
|
|
9
|
+
* file, and a verb's own options), each with exactly one enforcement point. A second copy of
|
|
10
|
+
* this list is how one of them drifts. */
|
|
11
|
+
const EVIDENCE_SCORE_FIELDS = new Set([
|
|
12
|
+
'confidence', 'score', 'relevance', 'relevanceScore', 'similarity',
|
|
13
|
+
'certainty', 'probability', 'rank', 'distance', 'weight',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
/** The one refusal message for a score offered as a property of a fact. */
|
|
17
|
+
function scoreFieldRefusal(name ) {
|
|
18
|
+
return `'${name}' is a retrieval score and this boundary records none (FR-007). Relevance is a `
|
|
19
|
+
+ 'property of a search, not of a fact: stored beside the fact it is read as confidence by the '
|
|
20
|
+
+ 'next worker, which is the specific interpretation the requirement forbids. Record the '
|
|
21
|
+
+ 'locator and the content instead — a reader who can see where a fact came from does not need '
|
|
22
|
+
+ 'to be told how sure to be.';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Refuses a score-shaped key anywhere in an item, including nested under source or locator. */
|
|
26
|
+
function assertNoScoreFields(value , prefix ) {
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return;
|
|
28
|
+
for (const [key, inner] of Object.entries(value )) {
|
|
29
|
+
const dotted = prefix ? `${prefix}.${key}` : key;
|
|
30
|
+
if (EVIDENCE_SCORE_FIELDS.has(key)) throw new Error(scoreFieldRefusal(dotted));
|
|
31
|
+
assertNoScoreFields(inner, dotted);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
EVIDENCE_SCORE_FIELDS,
|
|
37
|
+
scoreFieldRefusal,
|
|
38
|
+
assertNoScoreFields,
|
|
39
|
+
};
|
|
@@ -8,6 +8,7 @@ const crypto = require('node:crypto');
|
|
|
8
8
|
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
9
9
|
const { updateTaskState, readTaskState, mergeRecords } = require('./task-state.js');
|
|
10
10
|
const { taskStoreDir } = require('./task-scope.js');
|
|
11
|
+
const { assertNoScoreFields } = require('./evidence-fields.js');
|
|
11
12
|
|
|
12
13
|
const VALID_EVIDENCE_KINDS = new Set([
|
|
13
14
|
'exact-search',
|
|
@@ -129,6 +130,7 @@ class EvidenceLedger {
|
|
|
129
130
|
if (!item || typeof item !== 'object') {
|
|
130
131
|
throw new Error('Evidence item must be an object');
|
|
131
132
|
}
|
|
133
|
+
assertNoScoreFields(item, '');
|
|
132
134
|
|
|
133
135
|
if (!item.kind || !VALID_EVIDENCE_KINDS.has(item.kind)) {
|
|
134
136
|
throw new Error(`Invalid evidence kind '${item.kind}'. Allowed: ${[...VALID_EVIDENCE_KINDS].join(', ')}`);
|
|
@@ -152,6 +154,8 @@ class EvidenceLedger {
|
|
|
152
154
|
fileHash: item.freshness?.fileHash || null,
|
|
153
155
|
observedAt: item.freshness?.observedAt || new Date().toISOString(),
|
|
154
156
|
status: item.freshness?.status || 'FRESH',
|
|
157
|
+
isDirty: item.freshness?.isDirty !== undefined ? item.freshness.isDirty : null,
|
|
158
|
+
...(item.freshness?.treeFingerprint ? { treeFingerprint: item.freshness.treeFingerprint } : {}),
|
|
155
159
|
};
|
|
156
160
|
|
|
157
161
|
const evidenceRecord = {
|
|
@@ -163,6 +167,7 @@ class EvidenceLedger {
|
|
|
163
167
|
provenance,
|
|
164
168
|
content: item.content || null,
|
|
165
169
|
freshness,
|
|
170
|
+
...(item.result !== undefined ? { result: item.result } : {}),
|
|
166
171
|
// Which template requirements this item claims to establish. Readiness counts an item toward
|
|
167
172
|
// a requirement only when the writer named it here — kind-matching alone let an item saying
|
|
168
173
|
// "tests failed" satisfy a passing-baseline requirement (architecture review R1, P1).
|
|
@@ -208,6 +213,8 @@ class EvidenceLedger {
|
|
|
208
213
|
if (filter.capability && item.source.capability !== filter.capability) continue;
|
|
209
214
|
if (filter.file && item.locator.file !== filter.file) continue;
|
|
210
215
|
if (filter.status && item.freshness.status !== filter.status) continue;
|
|
216
|
+
if (filter.queryRef && item.source?.queryRef !== filter.queryRef) continue;
|
|
217
|
+
if (filter.attemptRef && item.source?.attemptRef !== filter.attemptRef) continue;
|
|
211
218
|
results.push(item);
|
|
212
219
|
}
|
|
213
220
|
return results;
|
package/src/runtime/freshness.js
CHANGED
|
@@ -7,6 +7,8 @@ const path = require('node:path');
|
|
|
7
7
|
const crypto = require('node:crypto');
|
|
8
8
|
const { execFileSync } = require('node:child_process');
|
|
9
9
|
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
10
|
+
const { measureTreeState } = require('./source-version.js');
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
// Freshness is measured against the bytes observed, not whether the file was clean at HEAD.
|
|
12
14
|
// Git identity remains provenance and a conservative fallback for older records without hashes.
|
|
@@ -25,17 +27,23 @@ function measureFreshness(root , locator , gitCommit ) {
|
|
|
25
27
|
|
|
26
28
|
class FreshnessValidator {
|
|
27
29
|
repoRoot ;
|
|
30
|
+
treeRoot ;
|
|
28
31
|
gitRunner ;
|
|
29
32
|
|
|
33
|
+
|
|
34
|
+
validatingLedger ;
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
37
|
* @param {Object} [options]
|
|
33
38
|
* @param {string} [options.repoRoot]
|
|
39
|
+
* @param {string} [options.treeRoot]
|
|
34
40
|
* @param {Function} [options.gitRunner]
|
|
35
41
|
*/
|
|
36
42
|
constructor(options = {}) {
|
|
37
43
|
this.repoRoot = options.repoRoot || REPO_ROOT;
|
|
44
|
+
this.treeRoot = options.treeRoot || this.repoRoot;
|
|
38
45
|
this.gitRunner = options.gitRunner || this.defaultGitRunner.bind(this);
|
|
46
|
+
this.validatingLedger = false;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
defaultGitRunner(args , cwd = this.repoRoot) {
|
|
@@ -121,6 +129,28 @@ class FreshnessValidator {
|
|
|
121
129
|
return { status: 'STALE', reason: 'Already marked stale' };
|
|
122
130
|
}
|
|
123
131
|
|
|
132
|
+
if (item.result === 'EMPTY' && item.freshness?.status === 'FRESH') {
|
|
133
|
+
let currentFingerprint;
|
|
134
|
+
if (this.validatingLedger) {
|
|
135
|
+
if (this.treeFingerprint === undefined) {
|
|
136
|
+
this.treeFingerprint = measureTreeState(this.treeRoot, this.gitRunner).fingerprint;
|
|
137
|
+
}
|
|
138
|
+
currentFingerprint = this.treeFingerprint;
|
|
139
|
+
} else {
|
|
140
|
+
currentFingerprint = measureTreeState(this.treeRoot, this.gitRunner).fingerprint;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!item.freshness?.treeFingerprint || !currentFingerprint) {
|
|
144
|
+
return { status: 'STALE', reason: 'tree-unverifiable' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (item.freshness.treeFingerprint !== currentFingerprint) {
|
|
148
|
+
return { status: 'STALE', reason: 'tree-changed-since-recorded' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { status: 'FRESH' };
|
|
152
|
+
}
|
|
153
|
+
|
|
124
154
|
const targetFile = item.locator?.file;
|
|
125
155
|
if (!targetFile) {
|
|
126
156
|
// Evidence without a file locator (e.g. general documentation) remains fresh
|
|
@@ -157,15 +187,22 @@ class FreshnessValidator {
|
|
|
157
187
|
|
|
158
188
|
let count = 0;
|
|
159
189
|
const allEvidence = ledger.getAllEvidence();
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
this.treeFingerprint = undefined;
|
|
191
|
+
this.validatingLedger = true;
|
|
192
|
+
try {
|
|
193
|
+
for (const item of allEvidence) {
|
|
194
|
+
if (item.freshness?.status === 'FRESH') {
|
|
195
|
+
const check = this.checkEvidenceFreshness(item);
|
|
196
|
+
if (check.status === 'STALE') {
|
|
197
|
+
item.freshness.status = 'STALE';
|
|
198
|
+
item.freshness.staleReason = check.reason;
|
|
199
|
+
count += 1;
|
|
200
|
+
}
|
|
167
201
|
}
|
|
168
202
|
}
|
|
203
|
+
} finally {
|
|
204
|
+
this.treeFingerprint = undefined;
|
|
205
|
+
this.validatingLedger = false;
|
|
169
206
|
}
|
|
170
207
|
return count;
|
|
171
208
|
}
|
package/src/runtime/outcome.js
CHANGED
|
@@ -55,7 +55,7 @@ const { planPath, UNREACHED } = require('./retrieval-plan.js');
|
|
|
55
55
|
// One definition of the score refusal, applied at a further boundary. See the note on
|
|
56
56
|
// EVIDENCE_SCORE_FIELDS in cli.js: a terminal state that carried a confidence would be exactly the
|
|
57
57
|
// number NFR-001 exists to keep out of the one record the next worker reads first.
|
|
58
|
-
const { assertNoScoreFields } = require('./
|
|
58
|
+
const { assertNoScoreFields } = require('./evidence-fields.js');
|
|
59
59
|
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
60
60
|
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
61
61
|
|
package/src/runtime/readiness.js
CHANGED
|
@@ -6,6 +6,7 @@ const fs = require('node:fs');
|
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const { parseYamlFile } = require('./capability-router.js');
|
|
8
8
|
const { resolveLocator, describeResolution } = require('./locator-resolve.js');
|
|
9
|
+
const { readPlan, bindEvidence, declaredIntentList } = require('./retrieval-record.js');
|
|
9
10
|
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
10
11
|
|
|
11
12
|
const READINESS_STATES = new Set([
|
|
@@ -211,7 +212,20 @@ class ReadinessEngine {
|
|
|
211
212
|
hasBlocked = true;
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
const
|
|
215
|
+
const allTaskEvidence = evidenceLedger ? evidenceLedger.queryEvidence({ taskId }) : [];
|
|
216
|
+
let plan = null;
|
|
217
|
+
let unreadablePlan = null;
|
|
218
|
+
try {
|
|
219
|
+
plan = readPlan(this.projectRoot, taskId);
|
|
220
|
+
} catch (error ) {
|
|
221
|
+
if (error.code !== 'store-unreadable') throw error;
|
|
222
|
+
unreadablePlan = { unreadable: true, reason: error.message };
|
|
223
|
+
}
|
|
224
|
+
const queryBound = Boolean(plan?.planId);
|
|
225
|
+
const binding = queryBound ? bindEvidence(plan, allTaskEvidence) : null;
|
|
226
|
+
const unboundIds = new Set(binding ? binding.unbound.map((item ) => item.id) : []);
|
|
227
|
+
const taskEvidence = allTaskEvidence.filter((item ) => item.freshness?.status === 'FRESH'
|
|
228
|
+
&& (!queryBound || !unboundIds.has(item.id)));
|
|
215
229
|
|
|
216
230
|
// An item accepted before FR-004 existed, or one whose target file has since shrunk, can be
|
|
217
231
|
// FRESH and still point at nothing. *Stale* (the file changed at all) and *unresolvable* (the
|
|
@@ -225,7 +239,8 @@ class ReadinessEngine {
|
|
|
225
239
|
// in evaluateClaim — it is not a claim that the file never changed. An item that genuinely went
|
|
226
240
|
// stale and was never superseded still forces NEEDS_EVIDENCE exactly as before.
|
|
227
241
|
const staleEvidence = (evidenceLedger ? evidenceLedger.getAllEvidence() : [])
|
|
228
|
-
.filter((item ) => item.taskId === taskId && item.
|
|
242
|
+
.filter((item ) => item.taskId === taskId && item.result !== 'EMPTY'
|
|
243
|
+
&& item.freshness?.status === 'STALE' && item.status !== 'superseded')
|
|
229
244
|
.map((item ) => ({ evidenceId: item.id, locator: item.locator, reason: 'file-modified-since-recorded' }));
|
|
230
245
|
|
|
231
246
|
const unresolvableEvidence = [];
|
|
@@ -242,9 +257,30 @@ class ReadinessEngine {
|
|
|
242
257
|
}
|
|
243
258
|
}
|
|
244
259
|
|
|
260
|
+
const staleAbsence = [];
|
|
261
|
+
const staleAbsenceByRequirement = new Map ();
|
|
262
|
+
for (const item of allTaskEvidence) {
|
|
263
|
+
if (item.result !== 'EMPTY' || item.status !== 'active'
|
|
264
|
+
|| !['STALE', 'UNKNOWN'].includes(item.freshness?.status)
|
|
265
|
+
|| (queryBound && unboundIds.has(item.id))) continue;
|
|
266
|
+
for (const requirement of Array.isArray(item.establishes) ? item.establishes : []) {
|
|
267
|
+
const entry = { evidenceId: item.id, requirement, provider: item.source?.provider || null,
|
|
268
|
+
freshness: item.freshness.status, refresh: plan?.providers?.[item.source?.provider]?.refresh || null };
|
|
269
|
+
staleAbsence.push(entry);
|
|
270
|
+
const entries = staleAbsenceByRequirement.get(requirement) || [];
|
|
271
|
+
entries.push({ item, entry });
|
|
272
|
+
staleAbsenceByRequirement.set(requirement, entries);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const unboundEvidence = queryBound
|
|
277
|
+
? { count: binding.unbound.length, ids: binding.unbound.slice(0, 5).map((item ) => item.id) }
|
|
278
|
+
: undefined;
|
|
279
|
+
|
|
245
280
|
for (const [reqId, reqDef] of Object.entries (requirements)) {
|
|
246
281
|
let isSatisfied = false;
|
|
247
282
|
let reason = null;
|
|
283
|
+
let reasonCode = null;
|
|
248
284
|
let matchedEvidenceIds = [];
|
|
249
285
|
|
|
250
286
|
// Check verification plan requirement
|
|
@@ -286,18 +322,18 @@ class ReadinessEngine {
|
|
|
286
322
|
// (review R1, P1 — reproduced through the public CLI).
|
|
287
323
|
else if (Array.isArray(reqDef.evidenceKinds) && reqDef.evidenceKinds.length > 0) {
|
|
288
324
|
const allowedKinds = new Set(reqDef.evidenceKinds);
|
|
325
|
+
const rawKindMatched = allTaskEvidence.filter((ev ) => allowedKinds.has(ev.kind));
|
|
289
326
|
const kindMatched = taskEvidence.filter((ev ) => allowedKinds.has(ev.kind));
|
|
290
327
|
const extracted = kindMatched.filter((ev ) => ev.provenance === 'extracted');
|
|
291
328
|
const bound = extracted.filter((ev ) => Array.isArray(ev.establishes) && ev.establishes.includes(reqId));
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
329
|
+
const observationOk = (ev ) => !reqDef.requiresObservation || (ev.observation
|
|
330
|
+
&& Number.isInteger(ev.observation.exitCode)
|
|
331
|
+
&& (reqDef.expectedExit !== 'zero' || ev.observation.exitCode === 0)
|
|
332
|
+
&& (reqDef.expectedExit !== 'nonzero' || ev.observation.exitCode !== 0));
|
|
333
|
+
const qualifies = (ev ) => ev.provenance === 'extracted'
|
|
334
|
+
&& Array.isArray(ev.establishes) && ev.establishes.includes(reqId)
|
|
335
|
+
&& observationOk(ev);
|
|
336
|
+
const qualifying = taskEvidence.filter((ev ) => allowedKinds.has(ev.kind) && qualifies(ev));
|
|
301
337
|
if (qualifying.length > 0) {
|
|
302
338
|
isSatisfied = true;
|
|
303
339
|
matchedEvidenceIds = qualifying.map((ev ) => ev.id);
|
|
@@ -319,6 +355,32 @@ class ReadinessEngine {
|
|
|
319
355
|
+ `${reqDef.expectedExit ? ` with a${reqDef.expectedExit === 'zero' ? ' passing (0)' : ' non-zero'} exit status` : ''} — `
|
|
320
356
|
+ 'record the execution itself, not a description of one.';
|
|
321
357
|
}
|
|
358
|
+
if (!isSatisfied && reqDef.required) {
|
|
359
|
+
const staleAbsences = staleAbsenceByRequirement.get(reqId) || [];
|
|
360
|
+
if (staleAbsences.length > 0) {
|
|
361
|
+
const { item, entry } = staleAbsences[0];
|
|
362
|
+
const refresh = entry.refresh || 'none declared';
|
|
363
|
+
reasonCode = 'stale-absence-unverified';
|
|
364
|
+
const prefix = `'${reqId}' rests only on an empty answer from ${entry.provider || 'unknown provider'}, whose index was ${entry.freshness} when it was recorded (${item.id}). An empty answer from that index cannot establish absence. `;
|
|
365
|
+
if (item.freshness?.staleReason === 'tree-changed-since-recorded'
|
|
366
|
+
|| item.freshness?.staleReason === 'tree-unverifiable') {
|
|
367
|
+
reason = `${prefix}the working tree changed since it was recorded; re-run the lookup and record the absence again.`;
|
|
368
|
+
} else if (item.freshness?.gitCommit === null) {
|
|
369
|
+
reason = `${prefix}absence cannot be established outside a git work tree; record a positive item.`;
|
|
370
|
+
} else {
|
|
371
|
+
reason = `${prefix}Run the declared refresh command (${refresh}) and record the absence again, or re-run the lookup with a live provider such as native.rg: doflow evidence --task-id ${taskId} --action add --absence --need <intent> --provider native.rg ...`;
|
|
372
|
+
}
|
|
373
|
+
} else if (queryBound) {
|
|
374
|
+
const unboundKind = rawKindMatched.filter((ev ) => unboundIds.has(ev.id));
|
|
375
|
+
const unboundQualifying = unboundKind.filter((ev ) =>
|
|
376
|
+
ev.freshness?.status === 'FRESH' && qualifies(ev));
|
|
377
|
+
if (unboundQualifying.length > 0) {
|
|
378
|
+
const ids = unboundQualifying.slice(0, 5).map((item ) => item.id).join(', ');
|
|
379
|
+
reasonCode = 'unbound-retrieval-evidence';
|
|
380
|
+
reason = `${unboundQualifying.length} item(s) of the right kind are not bound to a declared need of plan ${plan.planId} (${ids}), so they do not count. Re-record them with --need <intent>; declared: ${declaredIntentList(plan)}.`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
322
384
|
}
|
|
323
385
|
// Scope verified / scope clear
|
|
324
386
|
else if (reqId === 'scope_clear' || reqId === 'scope_verified' || reqId === 'invariants_captured') {
|
|
@@ -347,6 +409,7 @@ class ReadinessEngine {
|
|
|
347
409
|
required: Boolean(reqDef.required),
|
|
348
410
|
satisfied: isSatisfied,
|
|
349
411
|
reason,
|
|
412
|
+
reasonCode,
|
|
350
413
|
evidenceIds: matchedEvidenceIds,
|
|
351
414
|
recommendedAction: isSatisfied ? null : this.suggestAction(reqId, reqDef, taskProfile),
|
|
352
415
|
});
|
|
@@ -383,6 +446,10 @@ class ReadinessEngine {
|
|
|
383
446
|
+ 'Re-record them against the tree as it stands.';
|
|
384
447
|
}
|
|
385
448
|
|
|
449
|
+
if (unreadablePlan) {
|
|
450
|
+
summary += ` Warning: retrieval plan is unreadable and was treated as absent: ${unreadablePlan.reason}`;
|
|
451
|
+
}
|
|
452
|
+
|
|
386
453
|
return {
|
|
387
454
|
taskId,
|
|
388
455
|
taskClass,
|
|
@@ -399,6 +466,9 @@ class ReadinessEngine {
|
|
|
399
466
|
evidenceCount: taskEvidence.length,
|
|
400
467
|
unresolvableEvidence,
|
|
401
468
|
staleEvidence,
|
|
469
|
+
staleAbsence,
|
|
470
|
+
...(unreadablePlan ? { retrievalPlan: unreadablePlan } : {}),
|
|
471
|
+
...(unboundEvidence ? { unboundEvidence } : {}),
|
|
402
472
|
};
|
|
403
473
|
}
|
|
404
474
|
}
|
|
@@ -406,7 +476,7 @@ class ReadinessEngine {
|
|
|
406
476
|
/** One live snapshot for readiness inspection and orchestration transitions. Reads never persist
|
|
407
477
|
* derived freshness or claim status; every invocation evaluates the current project again.
|
|
408
478
|
* `mode` defaults to 'workflow' — fail closed: the standalone exemption must be declared. */
|
|
409
|
-
function evaluateTaskReadiness({ taskProfile, repoRoot = REPO_ROOT, projectRoot = process.cwd(), mode = 'workflow' } ) {
|
|
479
|
+
function evaluateTaskReadiness({ taskProfile, repoRoot = REPO_ROOT, projectRoot = process.cwd(), treeRoot = projectRoot, mode = 'workflow' } ) {
|
|
410
480
|
// Validated before any ledger work, not just inside stageEntryFor at the end — an invalid mode
|
|
411
481
|
// used to pay a full freshness/claims evaluation before erroring (review suggestion, 026).
|
|
412
482
|
if (!EXECUTION_MODES.has(mode)) {
|
|
@@ -417,7 +487,7 @@ function evaluateTaskReadiness({ taskProfile, repoRoot = REPO_ROOT, projectRoot
|
|
|
417
487
|
const { FreshnessValidator } = require('./freshness.js');
|
|
418
488
|
const ledger = new EvidenceLedger({ repoRoot: projectRoot });
|
|
419
489
|
ledger.load(taskProfile.taskId);
|
|
420
|
-
new FreshnessValidator({ repoRoot: projectRoot }).validateLedgerFreshness(ledger);
|
|
490
|
+
new FreshnessValidator({ repoRoot: projectRoot, treeRoot }).validateLedgerFreshness(ledger);
|
|
421
491
|
const claims = new ClaimsManager({ evidenceLedger: ledger, repoRoot: projectRoot });
|
|
422
492
|
claims.load(taskProfile.taskId);
|
|
423
493
|
claims.evaluateAll();
|