@tekyzinc/gsd-t 4.6.11 → 4.7.11
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/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/bin/gsd-t-architectural-trigger.cjs +523 -0
- package/bin/gsd-t-archive-domains.cjs +136 -0
- package/bin/gsd-t-loop-ledger.cjs +519 -0
- package/bin/gsd-t-research-gate.cjs +439 -0
- package/bin/gsd-t.js +82 -1
- package/commands/gsd-t-complete-milestone.md +15 -2
- package/commands/gsd-t-help.md +25 -0
- package/docs/requirements.md +17 -0
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +35 -16
- package/templates/prompts/blind-adversary-subagent.md +101 -0
- package/templates/prompts/research-subagent.md +127 -0
- package/templates/prompts/stated-claims-snippet.md +89 -0
- package/templates/test-helpers/launch-extension.ts +81 -0
- package/templates/workflows/gsd-t-debug.workflow.js +312 -0
- package/templates/workflows/gsd-t-execute.workflow.js +366 -1
- package/templates/workflows/gsd-t-phase.workflow.js +482 -5
- package/templates/workflows/gsd-t-quick.workflow.js +272 -2
- package/templates/workflows/gsd-t-verify.workflow.js +261 -1
|
@@ -2,16 +2,30 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Runtime: Anthropic native Workflow tool only.
|
|
4
4
|
// Quick — small one-shot task with contract awareness.
|
|
5
|
-
// preflight + brief + agent(task) + verify-gate (light)
|
|
5
|
+
// preflight + brief + agent(task) + research (M89 Stated-Claims) + verify-gate (light)
|
|
6
|
+
//
|
|
7
|
+
// M89: Stated-Claims→classify→research wiring (auto-research-contract v1.2.0 §6.5/§1/§2/§3/§4/§7).
|
|
8
|
+
// The task agent embeds the Stated-Claims snippet (§6.5) and tags load-bearing claims KNOWN|GUESSED.
|
|
9
|
+
// After the Execute phase, the Research phase iterates GUESSED entries through
|
|
10
|
+
// bin/gsd-t-research-gate.cjs: external → write §7 marker (status=uncited) → research agent
|
|
11
|
+
// (model:"fable") → cite → flip marker (status=cited). Internal → grep/Read; grep-empty →
|
|
12
|
+
// escalate to external (§5.1). Idempotent per §4.1 (exact normalized claim-key match only).
|
|
13
|
+
//
|
|
14
|
+
// M90 §2 D4-T4: Architectural-trigger wiring (protocol-class path, R-ARCH-2 "everywhere" feed).
|
|
15
|
+
// After the Research phase, compute the extend-existing-code signal from result.filesEdited:
|
|
16
|
+
// any file that already existed on disk → extend-class → fire the arch trigger (R-ARCH-2).
|
|
17
|
+
// NAMED PRODUCER: signal comes from result.filesEdited + on-disk existence check, never seeded.
|
|
18
|
+
// Contract: unproven-assumption-doctrine-contract.md §2 (R-ARCH-2, protocol-class path).
|
|
6
19
|
//
|
|
7
20
|
// args: { task, projectDir?, model? }
|
|
8
21
|
|
|
9
22
|
export const meta = {
|
|
10
23
|
name: "gsd-t-quick",
|
|
11
|
-
description: "Fast single-task execution with brief, preflight, and verify-gate",
|
|
24
|
+
description: "Fast single-task execution with brief, preflight, Stated-Claims research, and verify-gate",
|
|
12
25
|
phases: [
|
|
13
26
|
{ title: "Preflight", detail: "preflight + brief" },
|
|
14
27
|
{ title: "Execute", detail: "single-agent task" },
|
|
28
|
+
{ title: "Research", detail: "classify GUESSED claims + cite external (M89 §6.5/§1/§2/§7)" },
|
|
15
29
|
{ title: "Verify", detail: "verify-gate" },
|
|
16
30
|
],
|
|
17
31
|
};
|
|
@@ -81,9 +95,77 @@ const QUICK_SCHEMA = {
|
|
|
81
95
|
status: { type: "string", enum: ["complete", "partial", "blocked", "failed"] },
|
|
82
96
|
filesEdited: { type: "array", items: { type: "string" } },
|
|
83
97
|
summary: { type: "string" },
|
|
98
|
+
// M89 §6.5 — Stated Claims: raw [GUESSED:*] lines emitted by the task agent
|
|
99
|
+
statedClaims: { type: "array", items: { type: "string" } },
|
|
100
|
+
// Primary artifact path for §7 marker writes (optional)
|
|
101
|
+
artifactPath: { type: "string" },
|
|
84
102
|
},
|
|
85
103
|
};
|
|
86
104
|
|
|
105
|
+
// M89 §7 — normalize claim-key (§4.1 exact-match key). Cycle-2 finding #1: collapse
|
|
106
|
+
// EVERY non-word run to a space so the key is marker-syntax-safe; byte-identical across
|
|
107
|
+
// all 4 workflows (m89-normalize-claim-key-parity).
|
|
108
|
+
function normalizeClaimKey(claim) {
|
|
109
|
+
return claim.toLowerCase().replace(/[^\w]+/g, " ").trim();
|
|
110
|
+
}
|
|
111
|
+
function uncitedMarker(key) { return `<!-- auto-research-claim: class=external key=${key} status=uncited -->`; }
|
|
112
|
+
function citedMarker(key) { return `<!-- auto-research-claim: class=external key=${key} status=cited -->`; }
|
|
113
|
+
|
|
114
|
+
// M89 §1 — classify via D1 classifier (bin/gsd-t-research-gate.cjs)
|
|
115
|
+
async function classifyClaim(projectDir, claimText, phaseName) {
|
|
116
|
+
return runCli(projectDir, "research-gate", ["classify", claimText, "--json"], "gsd-t-research-gate.cjs", "classify-claim", true, phaseName);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// M89 §4.1 — exact key match idempotency check
|
|
120
|
+
function isAlreadyCited(artifactText, claimKey) {
|
|
121
|
+
return artifactText.includes(`auto-research-claim: class=external key=${claimKey} status=cited`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// M89 §5.1 — grep repo for internal claim resolution
|
|
125
|
+
async function grepForClaim(projectDir, claimText, phaseName) {
|
|
126
|
+
const escaped = claimText.replace(/'/g, "'\\''").slice(0, 200);
|
|
127
|
+
const prompt = [
|
|
128
|
+
`Decide whether THIS repo's own code/contracts/tests CONFIRM the SPECIFIC claim — not merely`,
|
|
129
|
+
`share vocabulary with it (Red Team MEDIUM). Project: \`${projectDir}\`. Claim: "${escaped}"`,
|
|
130
|
+
`Run: \`grep -r --include="*.js" --include="*.ts" --include="*.md" --include="*.cjs" -l "${escaped.slice(0, 60)}" "${projectDir}" 2>/dev/null | head -5\``,
|
|
131
|
+
`Then Read the candidates and judge: set found=true ONLY IF the repo content actually CONFIRMS`,
|
|
132
|
+
`the value/shape/behavior the claim asserts. Coincidental keyword overlap = found=false.`,
|
|
133
|
+
`If the claim is about an EXTERNAL system's behavior (3rd-party API, library, browser, protocol),`,
|
|
134
|
+
`grep CANNOT confirm it → found=false so it escalates to web research.`,
|
|
135
|
+
`Return JSON: { "found": <boolean>, "matches": ["<file>", ...] }. No other work.`,
|
|
136
|
+
].join("\n");
|
|
137
|
+
const schema = { type: "object", required: ["found"], additionalProperties: true, properties: { found: { type: "boolean" }, matches: { type: "array", items: { type: "string" } } } };
|
|
138
|
+
const r = await agent(prompt, { label: "grep-internal-claim", model: "haiku", schema, phase: phaseName })
|
|
139
|
+
.catch(() => ({ found: false, matches: [] }));
|
|
140
|
+
return r || { found: false, matches: [] };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// M89 §1.1 — AMBIGUOUS → LLM JUDGE (bare model:"fable"). internal/external/uncertain;
|
|
144
|
+
// uncertain → research (never guess-internal). On error → "uncertain" (fail toward research).
|
|
145
|
+
const CLASSIFY_JUDGE_SCHEMA = {
|
|
146
|
+
type: "object", required: ["verdict"], additionalProperties: true,
|
|
147
|
+
properties: { verdict: { type: "string", enum: ["internal", "external", "uncertain"] }, reason: { type: "string" } },
|
|
148
|
+
};
|
|
149
|
+
async function judgeAmbiguous(claimText, phaseName) {
|
|
150
|
+
const prompt = [
|
|
151
|
+
`You are the M89 ambiguous-claim JUDGE (auto-research-contract §1.1). The mechanical string-fact`,
|
|
152
|
+
`classifier could not decide this GUESSED claim. Decide in natural language. Claim: "${claimText}"`,
|
|
153
|
+
`- "internal" = about THIS repo's OWN code/contracts/tests — grep/Read can confirm.`,
|
|
154
|
+
`- "external" = asserts an OUTSIDE system's behavior/shape/limit — needs web research.`,
|
|
155
|
+
`- "uncertain" = you cannot CONFIDENTLY place it internal — per M89 doctrine it is RESEARCHED, never guessed.`,
|
|
156
|
+
`Return JSON: { "verdict": "internal"|"external"|"uncertain", "reason": "<one line>" }. No file/web work in THIS step.`,
|
|
157
|
+
].join("\n");
|
|
158
|
+
const r = await agent(prompt, { label: "classify-judge", model: "fable", schema: CLASSIFY_JUDGE_SCHEMA, phase: phaseName })
|
|
159
|
+
.catch((e) => ({ verdict: "uncertain", reason: `judge error: ${e && e.message}` }));
|
|
160
|
+
return (r && r.verdict) || "uncertain";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// M89 §2 — research result schema
|
|
164
|
+
const RESEARCH_RESULT_SCHEMA = {
|
|
165
|
+
type: "object", required: ["ok", "gapKey"], additionalProperties: true,
|
|
166
|
+
properties: { ok: { type: "boolean" }, gapKey: { type: "string" }, citedBlock: { type: "string" }, sourceUrls: { type: "array", items: { type: "string" } }, fetchDates: { type: "array", items: { type: "string" } }, reason: { type: "string" } },
|
|
167
|
+
};
|
|
168
|
+
|
|
87
169
|
if (!task) {
|
|
88
170
|
log("quick: args.task required");
|
|
89
171
|
return { status: "failed", reason: "no-task" };
|
|
@@ -107,6 +189,13 @@ const result = await agent(
|
|
|
107
189
|
`- Update relevant docs in the same commit`,
|
|
108
190
|
``,
|
|
109
191
|
`Commit with prefix "m61(quick)". Return JSON per the schema.`,
|
|
192
|
+
``,
|
|
193
|
+
`**M89 STATED CLAIMS — REQUIRED (auto-research-contract §6.5):**`,
|
|
194
|
+
`Read \`${projectDir}/templates/prompts/stated-claims-snippet.md\` for the DETECT protocol.`,
|
|
195
|
+
`Before returning, emit a \`## Stated Claims\` section tagging every load-bearing claim`,
|
|
196
|
+
`[KNOWN] or [GUESSED:assumed|unknown|stale]. In your StructuredOutput JSON, include a`,
|
|
197
|
+
`"statedClaims" array: one entry per [GUESSED:*] line (full bullet text). Include`,
|
|
198
|
+
`"artifactPath" if you wrote a primary output file (for §7 marker writes).`,
|
|
110
199
|
].join("\n"),
|
|
111
200
|
{ label: "quick", phase: "Execute", schema: QUICK_SCHEMA, model }
|
|
112
201
|
).catch((e) => ({ status: "failed", filesEdited: [], summary: `agent error: ${e && e.message}` }));
|
|
@@ -115,10 +204,191 @@ if (result.status === "failed" || result.status === "blocked") {
|
|
|
115
204
|
return { status: result.status, result };
|
|
116
205
|
}
|
|
117
206
|
|
|
207
|
+
// ── M89 Research Phase — Stated-Claims→classify→cite (§6.5/§1/§2/§3/§4/§7) ──
|
|
208
|
+
phase("Research");
|
|
209
|
+
const guessedClaims = [];
|
|
210
|
+
if (Array.isArray(result.statedClaims)) {
|
|
211
|
+
for (const claimLine of result.statedClaims) {
|
|
212
|
+
const m = claimLine.match(/^\[GUESSED:[^\]]+\]\s*(.+)$/);
|
|
213
|
+
if (m) guessedClaims.push({ claimText: m[1].trim(), artifactPath: result.artifactPath || null });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (guessedClaims.length === 0) {
|
|
218
|
+
log("Research: no GUESSED claims — skipping");
|
|
219
|
+
} else {
|
|
220
|
+
log(`Research: ${guessedClaims.length} GUESSED claim(s) to classify`);
|
|
221
|
+
const artifactCache = {};
|
|
222
|
+
async function readArtifact(p) {
|
|
223
|
+
if (!p) return "";
|
|
224
|
+
if (artifactCache[p] !== undefined) return artifactCache[p];
|
|
225
|
+
const r = await agent(`Read \`${p}\` and return JSON: { "text": "<file content>" }. If missing: { "text": "" }.`,
|
|
226
|
+
{ label: "read-artifact", model: "haiku", schema: { type: "object", required: ["text"], properties: { text: { type: "string" } }, additionalProperties: false }, phase: "Research" })
|
|
227
|
+
.catch(() => ({ text: "" }));
|
|
228
|
+
const t = (r && r.text) || "";
|
|
229
|
+
artifactCache[p] = t;
|
|
230
|
+
return t;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const { claimText, artifactPath } of guessedClaims) {
|
|
234
|
+
const claimKey = normalizeClaimKey(claimText);
|
|
235
|
+
|
|
236
|
+
// FAIL-CLOSED (Red Team HIGH): artifactPath is self-reported + optional. An EXTERNAL (or
|
|
237
|
+
// ambiguous→escalated-external) guess MUST get its §7 uncited marker written SOMEWHERE so the
|
|
238
|
+
// §7 ENFORCE gate has something to fail on. Use a DETERMINISTIC FALLBACK ARTIFACT when none.
|
|
239
|
+
const claimSlug = claimKey.replace(/\s+/g, "-").slice(0, 80) || "claim";
|
|
240
|
+
const externalArtifact = artifactPath || `${projectDir}/.gsd-t/research/quick-${claimSlug}.md`;
|
|
241
|
+
|
|
242
|
+
// §4.1 idempotency — read the path actually WRITTEN (externalArtifact = real OR fallback)
|
|
243
|
+
// so a re-run does not re-research a claim already cited in the fallback (Red Team MEDIUM).
|
|
244
|
+
const at = await readArtifact(externalArtifact);
|
|
245
|
+
if (isAlreadyCited(at, claimKey)) {
|
|
246
|
+
log(`Research: skip "${claimKey.slice(0, 50)}" — already cited`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const cls = await classifyClaim(projectDir, claimText, "Research");
|
|
251
|
+
const envelope = cls.envelope;
|
|
252
|
+
if (!envelope || !envelope.ok) {
|
|
253
|
+
log(`Research: classify error — ${cls.stderr || JSON.stringify(envelope)}`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// External-claim handler closure (reused by the ambiguous→judge path).
|
|
258
|
+
const doExternal = async () => {
|
|
259
|
+
log(`Research: external → marker + research(fable) for "${claimKey.slice(0, 50)}"${artifactPath ? "" : " (FALLBACK artifact — no reported path)"}`);
|
|
260
|
+
// §7 write uncited marker to the real OR fallback artifact — ALWAYS (fail-CLOSED)
|
|
261
|
+
{
|
|
262
|
+
const m = uncitedMarker(claimKey);
|
|
263
|
+
const sp = externalArtifact.replace(/'/g, "'\\''");
|
|
264
|
+
await agent(
|
|
265
|
+
`Ensure the parent dir exists, then append line \`${m}\` to \`${externalArtifact}\` if not already present. Use Bash: \`mkdir -p "$(dirname '${sp}')" && { grep -qF '${m.replace(/'/g, "'\\''")}' '${sp}' 2>/dev/null || echo '${m.replace(/'/g, "'\\''")}' >> '${sp}'; }\`. Return JSON: { "done": true }.`,
|
|
266
|
+
{ label: "write-uncited-marker", model: "haiku", schema: { type: "object", required: ["done"], properties: { done: { type: "boolean" } }, additionalProperties: true }, phase: "Research" }
|
|
267
|
+
).catch(() => {});
|
|
268
|
+
delete artifactCache[externalArtifact];
|
|
269
|
+
}
|
|
270
|
+
// §2 research agent — bare literal model: "fable"
|
|
271
|
+
const rr = await agent(
|
|
272
|
+
[
|
|
273
|
+
`Read \`${projectDir}/templates/prompts/research-subagent.md\` for the research protocol.`,
|
|
274
|
+
`Verify this external guessed claim: "${claimText}"`,
|
|
275
|
+
`Gap-key: "${claimKey}"`,
|
|
276
|
+
`Emit ## Verified Facts (auto-research) block with source URL + fetch date. Append the trailer \`key: ${claimKey}\` on every fact line so the §7 gate matches by claim-key (Red Team MEDIUM #2). Return StructuredOutput JSON.`,
|
|
277
|
+
].join("\n"),
|
|
278
|
+
{ label: "research", model: "fable", schema: RESEARCH_RESULT_SCHEMA, phase: "Research" }
|
|
279
|
+
).catch((e) => ({ ok: false, gapKey: claimKey, reason: String(e && e.message) }));
|
|
280
|
+
|
|
281
|
+
if (rr && rr.ok && rr.citedBlock) {
|
|
282
|
+
// §7 flip to cited + write cited block (real OR fallback artifact)
|
|
283
|
+
const um = uncitedMarker(claimKey);
|
|
284
|
+
const cm = citedMarker(claimKey);
|
|
285
|
+
await agent(
|
|
286
|
+
`Ensure the parent dir of \`${externalArtifact}\` exists (Bash: \`mkdir -p "$(dirname '${externalArtifact.replace(/'/g, "'\\''")}')"\`). Then Edit \`${externalArtifact}\`: replace the line \`${um}\` with \`${cm}\`, then append the following block if not already present:\n\`\`\`\n${rr.citedBlock}\n\`\`\`\nUse Read then Edit/Write tools. Return JSON: { "done": true, "action": "cited" }.`,
|
|
287
|
+
{ label: "flip-marker-cite", model: "haiku", schema: { type: "object", required: ["done"], properties: { done: { type: "boolean" }, action: { type: "string" } }, additionalProperties: true }, phase: "Research" }
|
|
288
|
+
).catch(() => {});
|
|
289
|
+
log(`Research: cited "${claimKey.slice(0, 50)}"`);
|
|
290
|
+
} else {
|
|
291
|
+
log(`Research: research failed for "${claimKey.slice(0, 50)}" — marker stays uncited`);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Internal-claim handler closure (grep A3; grep-empty → escalate §5.1).
|
|
296
|
+
const doInternal = async () => {
|
|
297
|
+
log(`Research: internal → grep for "${claimText.slice(0, 50)}"`);
|
|
298
|
+
const gr = await grepForClaim(projectDir, claimText, "Research");
|
|
299
|
+
if (gr.found) {
|
|
300
|
+
log(`Research: internal resolved by grep — no research needed`);
|
|
301
|
+
} else {
|
|
302
|
+
// §5.1 escalation: grep-empty → escalate to external. Reuse doExternal() (same
|
|
303
|
+
// marker→research→cite→flip path, incl. the key: trailer) instead of duplicating it.
|
|
304
|
+
log(`Research: grep empty — escalating to external (§5.1): "${claimText.slice(0, 50)}"${artifactPath ? "" : " (FALLBACK artifact)"}`);
|
|
305
|
+
await doExternal();
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// ── Dispatch by the 3-result classifier verdict (v1.3.0) ──────────────────
|
|
310
|
+
if (envelope.class === "external") {
|
|
311
|
+
await doExternal();
|
|
312
|
+
} else if (envelope.class === "internal") {
|
|
313
|
+
await doInternal();
|
|
314
|
+
} else {
|
|
315
|
+
// class:AMBIGUOUS — no string fact; LLM judge decides. internal→grep; external→research;
|
|
316
|
+
// UNCERTAIN→research (uncertain = verify, never guess-internal).
|
|
317
|
+
log(`Research: ambiguous → LLM judge for "${claimText.slice(0, 50)}" (when unsure, research)`);
|
|
318
|
+
const verdict = await judgeAmbiguous(claimText, "Research");
|
|
319
|
+
log(`Research: ambiguous "${claimKey.slice(0, 50)}" → judge: ${verdict}`);
|
|
320
|
+
if (verdict === "internal") {
|
|
321
|
+
await doInternal();
|
|
322
|
+
} else {
|
|
323
|
+
if (verdict === "uncertain") log(`Research: judge UNCERTAIN → "${claimKey.slice(0, 50)}" treated external → research (no silent guess)`);
|
|
324
|
+
await doExternal();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── M90 §2 D4-T4 — Arch-Trigger (protocol-class, extend-existing-code signal) ──
|
|
331
|
+
// Compute the extend-existing-code signal from result.filesEdited: files that already
|
|
332
|
+
// existed on disk before this quick run → extend-class → fire the arch trigger (R-ARCH-2).
|
|
333
|
+
// PRODUCER: signal is computed from real runtime inputs (filesEdited vs. on-disk check).
|
|
334
|
+
// Non-blocking: best-effort; don't halt quick on trigger failure.
|
|
335
|
+
let quickArchTriggerResult = null;
|
|
336
|
+
if (Array.isArray(result.filesEdited) && result.filesEdited.length > 0) {
|
|
337
|
+
const filesToCheck = result.filesEdited.slice(0, 10);
|
|
338
|
+
const checkResult = await agent(
|
|
339
|
+
[
|
|
340
|
+
`For the project at \`${projectDir}\`, check which of these files existed on disk BEFORE the current quick run.`,
|
|
341
|
+
`Files to check: ${JSON.stringify(filesToCheck)}`,
|
|
342
|
+
`For each file, run: \`test -f '${projectDir}/<file>' && echo EXISTS || echo ABSENT\` (substitute the actual path).`,
|
|
343
|
+
`Return JSON: { "existingFiles": ["<file>", ...] } — list ONLY files that exist (exit 0).`,
|
|
344
|
+
`Do NOT modify any files. Read-only existence check.`,
|
|
345
|
+
].join("\n"),
|
|
346
|
+
{
|
|
347
|
+
label: "arch-trigger-exist-check:quick",
|
|
348
|
+
phase: "Research",
|
|
349
|
+
model: "haiku",
|
|
350
|
+
schema: {
|
|
351
|
+
type: "object", required: ["existingFiles"],
|
|
352
|
+
properties: { existingFiles: { type: "array", items: { type: "string" } } },
|
|
353
|
+
additionalProperties: true,
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
).catch(() => ({ existingFiles: [] }));
|
|
357
|
+
|
|
358
|
+
const existingFiles = Array.isArray(checkResult && checkResult.existingFiles) ? checkResult.existingFiles : [];
|
|
359
|
+
if (existingFiles.length > 0) {
|
|
360
|
+
const triggerInput = JSON.stringify({
|
|
361
|
+
type: "extend-existing-code",
|
|
362
|
+
context: `quick-task edited existing files: ${existingFiles.slice(0, 3).join(", ")}`,
|
|
363
|
+
basis: `Quick task edited existing file(s): ${existingFiles.slice(0, 3).join(", ")}`,
|
|
364
|
+
});
|
|
365
|
+
const triggerResult = await runCli(
|
|
366
|
+
projectDir,
|
|
367
|
+
"architectural-trigger",
|
|
368
|
+
["trigger", triggerInput],
|
|
369
|
+
"gsd-t-architectural-trigger.cjs",
|
|
370
|
+
"arch-trigger:quick",
|
|
371
|
+
true,
|
|
372
|
+
"Research"
|
|
373
|
+
);
|
|
374
|
+
const triggerEnv = triggerResult.envelope || {};
|
|
375
|
+
log(`M90 arch-trigger quick: fired=${triggerEnv.fired} reason=${triggerEnv.reason || "?"} provenByAdversaryOnly=${triggerEnv.provenByAdversaryOnly || false}`);
|
|
376
|
+
quickArchTriggerResult = {
|
|
377
|
+
existingFiles,
|
|
378
|
+
fired: triggerEnv.fired || false,
|
|
379
|
+
reason: triggerEnv.reason || null,
|
|
380
|
+
provenByAdversaryOnly: triggerEnv.provenByAdversaryOnly || false,
|
|
381
|
+
stopDirective: triggerEnv.stopDirective || false,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
118
386
|
phase("Verify");
|
|
119
387
|
const vg = await runVerifyGate(projectDir);
|
|
120
388
|
return {
|
|
121
389
|
status: vg.ok ? "complete" : "verify-failed",
|
|
122
390
|
result,
|
|
123
391
|
verifyGate: vg.envelope,
|
|
392
|
+
// M90 §2: arch-trigger result (surfaced for verify gate R-FAIL-2 check)
|
|
393
|
+
archTriggerResult: quickArchTriggerResult,
|
|
124
394
|
};
|
|
@@ -6,9 +6,22 @@
|
|
|
6
6
|
// Canonical verify-phase Workflow. Replaces the gsd-t-verify command shell with
|
|
7
7
|
// a deterministic pipeline:
|
|
8
8
|
// preflight → brief → verify-gate (deterministic, hard-fail) →
|
|
9
|
+
// M89 §7 ENFORCE gate (auto-research-claim marker scan) →
|
|
9
10
|
// parallel( /code-review ultra cooperative, Red Team adversarial, QA mechanics ) →
|
|
10
11
|
// synthesis ( merge findings WITHOUT collapsing categories — see orthogonal-validation-contract.md )
|
|
11
12
|
//
|
|
13
|
+
// M89 §7 ENFORCE gate: an artifact carrying ANY <!-- auto-research-claim: ... status=uncited -->
|
|
14
|
+
// marker FAILs (an external guessed claim proceeded without a cited fact). All markers
|
|
15
|
+
// status=cited with matching ## Verified Facts (auto-research) entries → PASS.
|
|
16
|
+
// Contract: auto-research-contract.md §7 + §5 (A4).
|
|
17
|
+
//
|
|
18
|
+
// M90 §4 Fail-Closed gates (FAIL-blocking, after M89 §7 ENFORCE gate):
|
|
19
|
+
// R-FAIL-2: arch-trigger proven-by-adversary-only flag unresolved → VERIFY-FAILED.
|
|
20
|
+
// R-FAIL-3: loop-ledger haltedButNoReExamination state → VERIFY-FAILED.
|
|
21
|
+
// Both are DOCUMENTED no-op-PASSes when the producing mechanism is de-scoped (R1-EXIT),
|
|
22
|
+
// distinguishable from wired-but-broken vacuous passes. Never halt on a de-scoped mechanism.
|
|
23
|
+
// Contract: unproven-assumption-doctrine-contract.md §4 (R-FAIL-2, R-FAIL-3).
|
|
24
|
+
//
|
|
12
25
|
// args shape:
|
|
13
26
|
// {
|
|
14
27
|
// milestone: "M61",
|
|
@@ -19,10 +32,11 @@
|
|
|
19
32
|
export const meta = {
|
|
20
33
|
name: "gsd-t-verify",
|
|
21
34
|
description:
|
|
22
|
-
"GSD-T verify phase: preflight → brief → verify-gate → CI-parity gate (M57) → test-data purge gate (M58) → parallel(code-review-ultra, Red Team, QA) → synthesis",
|
|
35
|
+
"GSD-T verify phase: preflight → brief → verify-gate → M89 §7 ENFORCE gate → CI-parity gate (M57) → test-data purge gate (M58) → parallel(code-review-ultra, Red Team, QA) → synthesis",
|
|
23
36
|
phases: [
|
|
24
37
|
{ title: "Preflight", detail: "preflight + brief" },
|
|
25
38
|
{ title: "Verify-Gate", detail: "deterministic two-track verify-gate" },
|
|
39
|
+
{ title: "Auto-Research Gate", detail: "M89 §7 ENFORCE: scan for status=uncited markers (A4 — no silent guess)" },
|
|
26
40
|
{ title: "CI-Parity", detail: "M57 build-coverage + ci-parity (FAIL-blocking)" },
|
|
27
41
|
{ title: "Test-Data Purge", detail: "M58 test-data --purge (FAIL-blocking)" },
|
|
28
42
|
{ title: "Orthogonal Triad", detail: "code-review ultra ∥ Red Team ∥ QA" },
|
|
@@ -221,6 +235,251 @@ if (!vg.ok) {
|
|
|
221
235
|
}
|
|
222
236
|
log(`verify-gate green`);
|
|
223
237
|
|
|
238
|
+
// ─── M89 §7 ENFORCE Gate (FAIL-blocking — A4 no-silent-guess) ─────────────
|
|
239
|
+
// Scans all milestone artifacts for STRUCTURAL auto-research-claim markers and FAILs if:
|
|
240
|
+
// (a) ANY marker is status=uncited (an external guessed claim was never cited), OR
|
|
241
|
+
// (b) ANY status=cited marker's file lacks a matching `## Verified Facts (auto-research)`
|
|
242
|
+
// block with a sourced fact line (a "cited" marker with no backing fact — the
|
|
243
|
+
// hollow-gate defeat the contract §7/§5 A4 require to FAIL).
|
|
244
|
+
//
|
|
245
|
+
// CRITICAL STRUCTURAL RULE (Red Team #1 + code-review #1):
|
|
246
|
+
// - A marker counts ONLY if the LINE is a COMPLETE HTML comment
|
|
247
|
+
// `<!-- auto-research-claim: ... status=uncited|cited -->` (has `auto-research-claim:`
|
|
248
|
+
// AND `status=...` on the SAME line AND the line is the comment `<!-- ... -->`). A bare
|
|
249
|
+
// substring match would count the marker TEMPLATE string carried in this repo's own
|
|
250
|
+
// prose/source (CLAUDE-global.md, this contract, the workflows that EMIT the template)
|
|
251
|
+
// as live markers → spurious VERIFY-FAILED when verify dogfoods on this repo.
|
|
252
|
+
// - A status=cited marker is only honored if its file ALSO contains a
|
|
253
|
+
// `## Verified Facts (auto-research)` heading AND ≥1 sourced fact line (`source:` URL).
|
|
254
|
+
// A cited marker WITHOUT a matching sourced fact is counted as a violation (named by
|
|
255
|
+
// its claim-key) — same as an uncited marker. This closes the hollow-gate (A4).
|
|
256
|
+
// Contract: auto-research-contract.md §7 + §5 (A4).
|
|
257
|
+
// M81: no fs — delegate to an agent's Bash (grep) + parse result.
|
|
258
|
+
const AUTO_RESEARCH_GATE_SCHEMA = {
|
|
259
|
+
type: "object",
|
|
260
|
+
required: ["pass", "uncitedCount", "citedCount"],
|
|
261
|
+
additionalProperties: true,
|
|
262
|
+
properties: {
|
|
263
|
+
pass: { type: "boolean" },
|
|
264
|
+
uncitedCount: { type: "integer" },
|
|
265
|
+
citedCount: { type: "integer" },
|
|
266
|
+
citedWithoutFactsCount: { type: "integer" },
|
|
267
|
+
uncitedMarkers: { type: "array", items: { type: "string" } },
|
|
268
|
+
citedWithoutFactsKeys: { type: "array", items: { type: "string" } },
|
|
269
|
+
internalOnlyArtifacts: { type: "boolean" },
|
|
270
|
+
notes: { type: "string" },
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
phase("Auto-Research Gate");
|
|
275
|
+
const arGate = await agent(
|
|
276
|
+
[
|
|
277
|
+
`You are the M89 §7 ENFORCE gate scanner. Scan ALL milestone artifacts in the project at "${projectDir}" for STRUCTURAL auto-research-claim markers and determine pass/fail per contract §7 + §5 (A4 no-silent-guess).`,
|
|
278
|
+
``,
|
|
279
|
+
`A line is a LIVE MARKER ONLY IF it is a COMPLETE HTML comment of this exact structure (all on ONE line):`,
|
|
280
|
+
` <!-- auto-research-claim: class=external key=<some-key> status=uncited -->`,
|
|
281
|
+
` <!-- auto-research-claim: class=external key=<some-key> status=cited -->`,
|
|
282
|
+
`STRUCTURAL TEST (do NOT count a bare substring): the line, trimmed, MUST start with "<!--", MUST end with "-->", MUST contain "auto-research-claim:", MUST contain "status=uncited" or "status=cited", AND its key= value MUST be a CONCRETE normalized key (lowercase words/whitespace, NO angle brackets). Lines that merely MENTION the template in prose or source code (a backtick-quoted example, a string literal that emits the template, a contract illustration) are NOT live markers — they are not a standalone "<!-- ... -->" comment line, OR they carry a PLACEHOLDER key like "key=<claim-key>" / "key=<normalized-claim-key>" / "key=<some-key>" / "key=<key>" (any key= value containing "<" or ">"), OR a placeholder status like "status=uncited|cited" / "status=<status>". SKIP all of those — a real marker's key is the normalizeClaimKey output and never contains angle brackets.`,
|
|
283
|
+
``,
|
|
284
|
+
`Steps:`,
|
|
285
|
+
`1. Find candidate files: \`grep -rl "auto-research-claim" "${projectDir}" --include="*.md" --include="*.js" --include="*.ts" --include="*.tsx" --include="*.jsx" --include="*.cjs" --include="*.mjs" --include="*.py" 2>/dev/null || echo "no-matches"\``,
|
|
286
|
+
` NOTE: worker workflows (execute/quick/debug) write §7 markers into their primary OUTPUT artifact, which may be a code file (.js/.ts/.py/...), not only markdown — the include set MUST cover those or an uncited marker in a code artifact escapes the gate (A4).`,
|
|
287
|
+
`2. For each candidate file, read the lines containing "auto-research-claim". Apply the STRUCTURAL TEST above to each line; discard non-marker lines (template/prose/source illustrations).`,
|
|
288
|
+
`3. Among the SURVIVING live markers, classify each as uncited (status=uncited) or cited (status=cited), and record its key= value.`,
|
|
289
|
+
`4. uncitedCount = number of live status=uncited markers. List up to 10 of their full comment lines in "uncitedMarkers".`,
|
|
290
|
+
`5. For EACH live status=cited marker (with key=K): verify the SAME FILE contains a "## Verified Facts (auto-research)" heading AND a sourced fact line that BACKS this specific marker. A sourced fact line is a list item ("- ...") containing "source:" followed by a URL.`,
|
|
291
|
+
` PER-KEY MATCH (preferred — Red Team MEDIUM #2): if any sourced fact line in the file carries a "key: <value>" trailer, match cited markers to facts BY KEY — the cited marker key=K is backed ONLY by a sourced fact line whose "key:" value equals K (exact normalized-key match). A cited marker whose key has NO sourced fact line with a matching "key:" is HOLLOW even if other facts exist. This prevents one fact covering a DISTINCT claim by mere count.`,
|
|
292
|
+
` COUNT FALLBACK: if NO sourced fact line in the file carries a "key:" trailer, fall back to the count check — a cited marker is backed iff the file has at least as many sourced fact lines as it has cited markers.`,
|
|
293
|
+
` A cited marker in a file with NO Verified-Facts heading, OR with no backing fact under either rule above, is a HOLLOW cited marker — record its key= in "citedWithoutFactsKeys".`,
|
|
294
|
+
` citedCount = number of live status=cited markers. citedWithoutFactsCount = number of hollow cited markers (cited with no matching sourced fact).`,
|
|
295
|
+
`6. pass = true ONLY IF uncitedCount === 0 AND citedWithoutFactsCount === 0 (every external claim is cited AND every cited marker has a matching sourced Verified-Facts entry). pass = false otherwise.`,
|
|
296
|
+
`7. If no files are found (step 1 returns no-matches) OR no LIVE markers survive the structural test, pass=true, uncitedCount=0, citedCount=0, citedWithoutFactsCount=0.`,
|
|
297
|
+
``,
|
|
298
|
+
`Return JSON per the schema: { "pass": true|false, "uncitedCount": N, "citedCount": N, "citedWithoutFactsCount": N, "uncitedMarkers": [], "citedWithoutFactsKeys": [], "notes": "..." }`,
|
|
299
|
+
``,
|
|
300
|
+
`Contract: auto-research-contract.md §7 (ENFORCE marker — cited REQUIRES a matching Verified-Facts entry, same claim-key) + §5/A4 (uncited or hollow-cited external guess → verify FAILS).`,
|
|
301
|
+
].join("\n"),
|
|
302
|
+
{ label: "auto-research-gate", phase: "Auto-Research Gate", schema: AUTO_RESEARCH_GATE_SCHEMA, model: "haiku" }
|
|
303
|
+
).catch((e) => ({
|
|
304
|
+
pass: false, uncitedCount: -1, citedCount: 0, citedWithoutFactsCount: -1,
|
|
305
|
+
notes: `auto-research gate agent error: ${e && e.message} — failing closed (A4)`,
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
if (!arGate.pass) {
|
|
309
|
+
const uncited = arGate.uncitedCount >= 0 ? arGate.uncitedCount : "unknown (agent error)";
|
|
310
|
+
const hollow = arGate.citedWithoutFactsCount >= 0 ? arGate.citedWithoutFactsCount : "unknown (agent error)";
|
|
311
|
+
log(`M89 auto-research gate FAIL — ${uncited} uncited + ${hollow} hollow-cited external-claim marker(s) (A4: no silent guess)`);
|
|
312
|
+
if (arGate.uncitedMarkers && arGate.uncitedMarkers.length > 0) {
|
|
313
|
+
log(` uncited markers: ${arGate.uncitedMarkers.slice(0, 5).join(" | ")}`);
|
|
314
|
+
}
|
|
315
|
+
if (arGate.citedWithoutFactsKeys && arGate.citedWithoutFactsKeys.length > 0) {
|
|
316
|
+
log(` hollow cited (no matching sourced fact) keys: ${arGate.citedWithoutFactsKeys.slice(0, 5).join(" | ")}`);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
status: "auto-research-gate-failed",
|
|
320
|
+
overallVerdict: "VERIFY-FAILED",
|
|
321
|
+
autoResearchGate: arGate,
|
|
322
|
+
reason: `M89 §7 ENFORCE: ${uncited} uncited + ${hollow} hollow-cited external-claim marker(s) — every cited marker needs a matching sourced Verified-Facts entry; run the phase again to cite them`,
|
|
323
|
+
verifyGate: vg.envelope,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
log(`M89 auto-research gate: PASS — ${arGate.citedCount} cited marker(s) all backed by sourced facts, 0 uncited`);
|
|
327
|
+
|
|
328
|
+
// ─── M90 §4 Fail-Closed Gates (FAIL-blocking) ─────────────────────────────
|
|
329
|
+
// R-FAIL-2: arch-trigger produced a proven-by-adversary-only flag that was never resolved.
|
|
330
|
+
// R-FAIL-3: loop-ledger has a halted-but-no-re-examination state (non-convergence).
|
|
331
|
+
//
|
|
332
|
+
// DOCUMENTED no-op-PASS rules (§4 — de-scoped-DOWN vs. wired-but-broken):
|
|
333
|
+
// - R-FAIL-2 read is a NO-OP-PASS when the arch-trigger is NOT wired (R1-EXIT de-scoped-DOWN).
|
|
334
|
+
// Documented as "mechanism absent by design" — distinguishable from a wired-but-broken state.
|
|
335
|
+
// - R-FAIL-3 read is a NO-OP-PASS when the loop-ledger halt is NOT wired (de-scoped).
|
|
336
|
+
// Documented as "mechanism absent by design" — distinguishable from a wired-but-broken state.
|
|
337
|
+
//
|
|
338
|
+
// The checks FAIL only when the mechanism IS wired AND emits an unresolved flag.
|
|
339
|
+
// They cleanly PASS (with a recorded de-scoped note) when the mechanism is absent.
|
|
340
|
+
// Contract: unproven-assumption-doctrine-contract.md §4 (R-FAIL-2, R-FAIL-3).
|
|
341
|
+
|
|
342
|
+
// R-FAIL-2: arch-trigger proven-by-adversary-only premise gate (§4).
|
|
343
|
+
// Three DISTINGUISHABLE states (unproven-assumption-doctrine-contract.md §4 + §2.2):
|
|
344
|
+
// (a) interfaceOnly PASS — NO live producer of the flag wired this milestone (no workflow sets
|
|
345
|
+
// spikeFeasible), so the flag can't be raised in real operation. DECLARED (M90 decision A),
|
|
346
|
+
// not a hidden hollow gate. → grep the workflows for a live producer to detect this.
|
|
347
|
+
// (b) deScopedPass — R1 exited to factual-only, trigger not wired at all.
|
|
348
|
+
// (c) FAIL — a live producer exists AND emitted an unresolved proven-by-adversary-only flag.
|
|
349
|
+
// The gate must NEVER vacuously pass a wired-but-broken check. When backlog #42 wires a live
|
|
350
|
+
// spike-feasibility producer, the grep finds it and the gate becomes (c)-capable.
|
|
351
|
+
const M90_ARCH_TRIGGER_SINK = `${projectDir}/.gsd-t/metrics/arch-trigger-events.jsonl`;
|
|
352
|
+
const m90ArchTriggerGate = await agent(
|
|
353
|
+
[
|
|
354
|
+
`You are the M90 §4 R-FAIL-2 gate scanner. Determine which of three states holds and return JSON.`,
|
|
355
|
+
``,
|
|
356
|
+
`Sink file: \`${M90_ARCH_TRIGGER_SINK}\``,
|
|
357
|
+
``,
|
|
358
|
+
`Steps:`,
|
|
359
|
+
`1. LIVE-PRODUCER CHECK: a real producer PASSES a spike-feasibility decision INTO the trigger,`,
|
|
360
|
+
` i.e. a workflow constructs \`responseOpts\` / sets \`spikeFeasible\` as an OBJECT KEY in a call.`,
|
|
361
|
+
` Run (note: EXCLUDE this verify workflow itself — its prompt text mentions these words but does`,
|
|
362
|
+
` NOT produce the flag, the self-match bug being avoided):`,
|
|
363
|
+
` \`grep -lE 'spikeFeasible[ ]*:|spikePassed[ ]*:|responseOpts[ ]*[:=]' ${projectDir}/templates/workflows/*.workflow.js 2>/dev/null | grep -v 'gsd-t-verify.workflow.js' | head\`.`,
|
|
364
|
+
` If it prints NOTHING → there is NO live producer of the proven-by-adversary-only flag wired`,
|
|
365
|
+
` this milestone. Return (INTERFACE-ONLY, declared PASS per §2.2 / decision A):`,
|
|
366
|
+
` { "pass": true, "interfaceOnly": true, "deScopedPass": false, "provenByAdversaryOnlyCount": 0,`,
|
|
367
|
+
` "note": "R-FAIL-2 interface-only this milestone — no workflow sets spikeFeasible, so proven-by-adversary-only is never raised; DECLARED no-op-PASS (doctrine contract §2.2/§4, backlog #42 wires the live producer)" }`,
|
|
368
|
+
`2. If a live producer IS found → check the sink: \`test -f '${M90_ARCH_TRIGGER_SINK}' && echo EXISTS || echo ABSENT\`.`,
|
|
369
|
+
` - ABSENT → trigger genuinely de-scoped (R1-EXIT). Return:`,
|
|
370
|
+
` { "pass": true, "interfaceOnly": false, "deScopedPass": true, "provenByAdversaryOnlyCount": 0, "note": "arch-trigger mechanism absent by design (R1 de-scoped-DOWN) — documented no-op-PASS" }`,
|
|
371
|
+
` - EXISTS → read each JSONL line; count records where provenByAdversaryOnly === true → provenByAdversaryOnlyCount.`,
|
|
372
|
+
` pass = (provenByAdversaryOnlyCount === 0). Return:`,
|
|
373
|
+
` { "pass": boolean, "interfaceOnly": false, "deScopedPass": false, "provenByAdversaryOnlyCount": N, "note": "..." }`,
|
|
374
|
+
``,
|
|
375
|
+
`This gate is R-FAIL-2 per unproven-assumption-doctrine-contract.md §4. The interfaceOnly PASS and`,
|
|
376
|
+
`deScopedPass are BOTH distinguishable from a wired-but-broken vacuous pass — they are only`,
|
|
377
|
+
`returned when the producer is provably absent (grep) / de-scoped, never to mask a wired failure.`,
|
|
378
|
+
].join("\n"),
|
|
379
|
+
{
|
|
380
|
+
label: "m90-r-fail-2-gate",
|
|
381
|
+
phase: "Auto-Research Gate",
|
|
382
|
+
model: "haiku",
|
|
383
|
+
schema: {
|
|
384
|
+
type: "object",
|
|
385
|
+
required: ["pass"],
|
|
386
|
+
additionalProperties: true,
|
|
387
|
+
properties: {
|
|
388
|
+
pass: { type: "boolean" },
|
|
389
|
+
interfaceOnly: { type: "boolean" },
|
|
390
|
+
deScopedPass: { type: "boolean" },
|
|
391
|
+
provenByAdversaryOnlyCount: { type: "integer" },
|
|
392
|
+
note: { type: "string" },
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
).catch((e) => ({
|
|
397
|
+
pass: false,
|
|
398
|
+
deScopedPass: false,
|
|
399
|
+
provenByAdversaryOnlyCount: -1,
|
|
400
|
+
note: `M90 R-FAIL-2 gate agent error: ${e && e.message} — failing closed (§4)`,
|
|
401
|
+
}));
|
|
402
|
+
|
|
403
|
+
if (!m90ArchTriggerGate.pass) {
|
|
404
|
+
const count = m90ArchTriggerGate.provenByAdversaryOnlyCount >= 0
|
|
405
|
+
? m90ArchTriggerGate.provenByAdversaryOnlyCount
|
|
406
|
+
: "unknown (agent error)";
|
|
407
|
+
log(`M90 R-FAIL-2 gate FAIL — ${count} unresolved proven-by-adversary-only flag(s) (§4 fail-closed)`);
|
|
408
|
+
return {
|
|
409
|
+
status: "m90-r-fail-2-failed",
|
|
410
|
+
overallVerdict: "VERIFY-FAILED",
|
|
411
|
+
m90ArchTriggerGate,
|
|
412
|
+
reason: `M90 §4 R-FAIL-2: ${count} unresolved proven-by-adversary-only premise(s) — re-examine the premise, then re-verify`,
|
|
413
|
+
verifyGate: vg.envelope,
|
|
414
|
+
autoResearchGate: arGate,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
log(`M90 R-FAIL-2 gate: PASS — ${m90ArchTriggerGate.deScopedPass ? "de-scoped (mechanism absent by design)" : `0 unresolved proven-by-adversary-only flag(s)`}`);
|
|
418
|
+
|
|
419
|
+
// R-FAIL-3: read loop-ledger exit state (haltedButNoReExamination flag)
|
|
420
|
+
const m90LoopLedgerGate = await runCli(
|
|
421
|
+
projectDir,
|
|
422
|
+
"loop-ledger",
|
|
423
|
+
["read-exit-state", ...(milestone ? ["--milestone", milestone] : []), "--projectDir", projectDir],
|
|
424
|
+
"gsd-t-loop-ledger.cjs",
|
|
425
|
+
"m90-r-fail-3-gate",
|
|
426
|
+
true,
|
|
427
|
+
"Auto-Research Gate"
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// Determine pass/fail from the loop-ledger exit state. §4 = FAIL-CLOSED.
|
|
431
|
+
// haltedButNoReExamination=true → FAIL (an unresolved non-converging loop)
|
|
432
|
+
// envelope.ok=false (CORRUPT state file, exit 1) → FAIL (fail-closed: a corrupt ledger must
|
|
433
|
+
// NEVER pass — the module deliberately raises this; treating it as "de-scoped" was a
|
|
434
|
+
// fail-OPEN inversion, Red Team HIGH M90 verify fix-cycle 2)
|
|
435
|
+
// genuinely-absent MODULE (no envelope at all + run/CLI error) → de-scoped NO-OP-PASS
|
|
436
|
+
// ok=true, haltedButNoReExamination=false → PASS (clean)
|
|
437
|
+
const exitEnv = m90LoopLedgerGate.envelope || null;
|
|
438
|
+
let m90LoopLedgerPass = true;
|
|
439
|
+
let m90LoopLedgerNote = "";
|
|
440
|
+
if (exitEnv && exitEnv.ok === false) {
|
|
441
|
+
// The CLI RAN and returned a structured error envelope → the WIRED mechanism is failing
|
|
442
|
+
// closed (corrupt/unreadable state). FAIL — do NOT convert a deliberate fail-closed into a pass.
|
|
443
|
+
m90LoopLedgerPass = false;
|
|
444
|
+
m90LoopLedgerNote = `R-FAIL-3: loop-ledger returned a fail-closed error (${exitEnv.error || "corrupt/unreadable state"}) — a corrupt ledger must block verify, not pass §4`;
|
|
445
|
+
log(`M90 R-FAIL-3 gate FAIL — loop-ledger fail-closed error (§4): ${exitEnv.error || "corrupt state"}`);
|
|
446
|
+
} else if (!exitEnv && !m90LoopLedgerGate.ok) {
|
|
447
|
+
// No parseable envelope AND the run errored → the MODULE itself is genuinely absent/unrunnable
|
|
448
|
+
// (not a corrupt state file). This is the true R1-EXIT de-scoped no-op-PASS.
|
|
449
|
+
m90LoopLedgerPass = true;
|
|
450
|
+
m90LoopLedgerNote = "loop-ledger module genuinely absent (no envelope) — R-FAIL-3 de-scoped no-op-PASS";
|
|
451
|
+
log(`M90 R-FAIL-3 gate: PASS (de-scoped — loop-ledger module absent / not wired)`);
|
|
452
|
+
} else if (exitEnv && exitEnv.ok && exitEnv.haltedButNoReExamination) {
|
|
453
|
+
m90LoopLedgerPass = false;
|
|
454
|
+
// RECOVERABILITY (M90 verify decision A): name the exact signature(s) + the one-line clear
|
|
455
|
+
// command, so a halt is resolvable — never a cryptic count with no remedy (the cross-milestone
|
|
456
|
+
// brick was unrecoverable because the FAIL note printed only a number).
|
|
457
|
+
const pend = exitEnv.pendingSignatures || exitEnv.haltedSignatures || [];
|
|
458
|
+
const clearCmds = pend
|
|
459
|
+
.map((s) => `gsd-t loop-ledger record-re-examination --signature ${s} --projectDir ${projectDir}`)
|
|
460
|
+
.join(" | ");
|
|
461
|
+
m90LoopLedgerNote =
|
|
462
|
+
`R-FAIL-3: ${pend.length} unresolved debug-loop halt(s) for this milestone — re-examine the premise per §3.2, then clear: ${clearCmds}`;
|
|
463
|
+
log(`M90 R-FAIL-3 gate FAIL — unresolved halt(s): ${pend.join(", ")}`);
|
|
464
|
+
log(`To resolve after re-examination: ${clearCmds}`);
|
|
465
|
+
} else {
|
|
466
|
+
m90LoopLedgerPass = true;
|
|
467
|
+
m90LoopLedgerNote = `no halted-but-no-re-examination state (haltedButNoReExamination=${(exitEnv && exitEnv.haltedButNoReExamination) || false})`;
|
|
468
|
+
log(`M90 R-FAIL-3 gate: PASS — ${m90LoopLedgerNote}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!m90LoopLedgerPass) {
|
|
472
|
+
return {
|
|
473
|
+
status: "m90-r-fail-3-failed",
|
|
474
|
+
overallVerdict: "VERIFY-FAILED",
|
|
475
|
+
m90LoopLedgerGate: m90LoopLedgerGate.envelope,
|
|
476
|
+
reason: m90LoopLedgerNote,
|
|
477
|
+
verifyGate: vg.envelope,
|
|
478
|
+
autoResearchGate: arGate,
|
|
479
|
+
m90ArchTriggerGate,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
224
483
|
// ─── M57 CI-Parity Gate (FAIL-blocking) ───────────────────────────────────
|
|
225
484
|
// Per commands/gsd-t-verify.md Step 2.6 + cli-build-coverage-contract.md +
|
|
226
485
|
// ci-parity-contract.md. NEITHER track is currently inside verify-gate.cjs.
|
|
@@ -366,6 +625,7 @@ return {
|
|
|
366
625
|
status: verdict.overallVerdict === "VERIFY-FAILED" ? "failed" : "complete",
|
|
367
626
|
overallVerdict: verdict.overallVerdict,
|
|
368
627
|
verifyGate: vg.envelope,
|
|
628
|
+
autoResearchGate: arGate,
|
|
369
629
|
buildCoverage: bc.envelope,
|
|
370
630
|
ciParity: cip.envelope,
|
|
371
631
|
testDataPurge: td.envelope,
|