@tekyzinc/gsd-t 4.6.11 → 4.7.10
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 +18 -0
- package/README.md +1 -1
- package/bin/gsd-t-architectural-trigger.cjs +523 -0
- package/bin/gsd-t-loop-ledger.cjs +519 -0
- package/bin/gsd-t-research-gate.cjs +439 -0
- package/bin/gsd-t.js +63 -1
- 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
|
@@ -4,6 +4,25 @@
|
|
|
4
4
|
// Debug phase — up to 2 fix cycles per CLAUDE.md Prime Rule. If still failing
|
|
5
5
|
// after 2 cycles, exit with `needs-human` so the human operator can step in.
|
|
6
6
|
//
|
|
7
|
+
// M89: Stated-Claims→classify→research wiring (auto-research-contract v1.2.0 §6.5/§1/§2/§3/§4/§7).
|
|
8
|
+
// Each debug cycle embeds the Stated-Claims snippet (§6.5) so the debug agent tags failure-root
|
|
9
|
+
// claims KNOWN|GUESSED. After each cycle, GUESSED claims are classified: external failure-root
|
|
10
|
+
// → write §7 marker (status=uncited) → research agent (model:"fable") → cite → flip marker
|
|
11
|
+
// (status=cited) — instead of a patch-guess (pairs with #33 circuit-breaker). Internal →
|
|
12
|
+
// grep/Read; grep-empty → escalate to external (§5.1). The existing debug-cycle ternary
|
|
13
|
+
// (model: cycle===1?"opus":(overrides["debug-cycle-2"]??"fable")) is PRESERVED; the research
|
|
14
|
+
// stage is a SEPARATE agent() with its own bare "fable" literal. Idempotent per §4.1.
|
|
15
|
+
//
|
|
16
|
+
// M90 §3 D4-T3: Loop-ledger halt wired via inline runCli helper (option b).
|
|
17
|
+
// After each cycle, append-cycle is called with the symptom as the assertion, the primary
|
|
18
|
+
// edited file as the surface, and "unit" as the fileClass. After cycle 2 (end of loop),
|
|
19
|
+
// read-exit-state is called; if haltedButNoReExamination=true (the same computed signature
|
|
20
|
+
// appeared across both cycles — the ledger proves non-convergence), the workflow exits at
|
|
21
|
+
// the cycle-2 boundary with the ledger's PREMISE_RE_EXAMINATION directive rather than the
|
|
22
|
+
// generic `needs-human`. This is option (b): re-anchor the halt to the cycle-2 boundary,
|
|
23
|
+
// keeping the existing 2-cycle cap unchanged and only changing the EXIT REASON when the
|
|
24
|
+
// ledger fact proves non-convergence. Contract: unproven-assumption-doctrine-contract.md §3.2.
|
|
25
|
+
//
|
|
7
26
|
// args: { symptom, projectDir? }
|
|
8
27
|
|
|
9
28
|
export const meta = {
|
|
@@ -55,6 +74,7 @@ async function generateBrief(projectDir, { kind = "execute", milestone, domain,
|
|
|
55
74
|
|
|
56
75
|
const projectDir = _args.projectDir || ".";
|
|
57
76
|
const symptom = _args.symptom || null;
|
|
77
|
+
const milestone = _args.milestone || null; // tags loop-ledger halts so they scope to this milestone
|
|
58
78
|
|
|
59
79
|
const DEBUG_CYCLE_SCHEMA = {
|
|
60
80
|
type: "object",
|
|
@@ -68,9 +88,198 @@ const DEBUG_CYCLE_SCHEMA = {
|
|
|
68
88
|
properties: { pass: { type: "integer" }, fail: { type: "integer" } },
|
|
69
89
|
},
|
|
70
90
|
nextStepsIfNotResolved: { type: "string" },
|
|
91
|
+
// M89 §6.5 — Stated Claims: raw [GUESSED:*] lines from the debug cycle agent
|
|
92
|
+
statedClaims: { type: "array", items: { type: "string" } },
|
|
93
|
+
// Primary artifact path for §7 marker writes (e.g. the file being debugged / a debug report)
|
|
94
|
+
artifactPath: { type: "string" },
|
|
71
95
|
},
|
|
72
96
|
};
|
|
73
97
|
|
|
98
|
+
// M89 §7 — normalize claim-key (§4.1 exact-match key). Cycle-2 finding #1: collapse
|
|
99
|
+
// EVERY non-word run to a space so the key is marker-syntax-safe; byte-identical across
|
|
100
|
+
// all 4 workflows (m89-normalize-claim-key-parity).
|
|
101
|
+
function normalizeClaimKey(claim) {
|
|
102
|
+
return claim.toLowerCase().replace(/[^\w]+/g, " ").trim();
|
|
103
|
+
}
|
|
104
|
+
function uncitedMarker(key) { return `<!-- auto-research-claim: class=external key=${key} status=uncited -->`; }
|
|
105
|
+
function citedMarker(key) { return `<!-- auto-research-claim: class=external key=${key} status=cited -->`; }
|
|
106
|
+
|
|
107
|
+
// M89 §1 — classify via bin/gsd-t-research-gate.cjs (D1 envelope)
|
|
108
|
+
async function classifyClaim(projectDir, claimText, phaseName) {
|
|
109
|
+
return runCli(projectDir, "research-gate", ["classify", claimText, "--json"], "gsd-t-research-gate.cjs", "classify-claim", true, phaseName);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// M89 §4.1 — exact key match idempotency
|
|
113
|
+
function isAlreadyCited(artifactText, claimKey) {
|
|
114
|
+
return artifactText.includes(`auto-research-claim: class=external key=${claimKey} status=cited`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// M89 §5.1 — grep repo for internal claim resolution
|
|
118
|
+
async function grepForClaim(projectDir, claimText, phaseName) {
|
|
119
|
+
const escaped = claimText.replace(/'/g, "'\\''").slice(0, 200);
|
|
120
|
+
const prompt = [
|
|
121
|
+
`Decide whether THIS repo's own code/contracts/tests CONFIRM the SPECIFIC claim — not merely`,
|
|
122
|
+
`share vocabulary with it (Red Team MEDIUM). Project: \`${projectDir}\`. Claim: "${escaped}"`,
|
|
123
|
+
`Run: \`grep -r --include="*.js" --include="*.ts" --include="*.md" --include="*.cjs" -l "${escaped.slice(0, 60)}" "${projectDir}" 2>/dev/null | head -5\``,
|
|
124
|
+
`Then Read the candidates and judge: set found=true ONLY IF the repo content actually CONFIRMS`,
|
|
125
|
+
`the value/shape/behavior the claim asserts. Coincidental keyword overlap = found=false.`,
|
|
126
|
+
`If the claim is about an EXTERNAL system's behavior (3rd-party API, library, browser, protocol),`,
|
|
127
|
+
`grep CANNOT confirm it → found=false so it escalates to web research.`,
|
|
128
|
+
`Return JSON: { "found": <boolean>, "matches": ["<file>", ...] }. No other work.`,
|
|
129
|
+
].join("\n");
|
|
130
|
+
const schema = { type: "object", required: ["found"], additionalProperties: true, properties: { found: { type: "boolean" }, matches: { type: "array", items: { type: "string" } } } };
|
|
131
|
+
const r = await agent(prompt, { label: "grep-internal-claim", model: "haiku", schema, phase: phaseName }).catch(() => ({ found: false, matches: [] }));
|
|
132
|
+
return r || { found: false, matches: [] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// M89 §1.1 — AMBIGUOUS → LLM JUDGE (bare model:"fable" — NOT the ?? form; "judge" is not
|
|
136
|
+
// a designated stage, so the bare literal passes the M85 tier-set check and stays DISTINCT
|
|
137
|
+
// from the debug-cycle ternary). internal/external/uncertain; uncertain → research (never
|
|
138
|
+
// guess-internal). On error → "uncertain" (fail toward research).
|
|
139
|
+
const CLASSIFY_JUDGE_SCHEMA = {
|
|
140
|
+
type: "object", required: ["verdict"], additionalProperties: true,
|
|
141
|
+
properties: { verdict: { type: "string", enum: ["internal", "external", "uncertain"] }, reason: { type: "string" } },
|
|
142
|
+
};
|
|
143
|
+
async function judgeAmbiguous(claimText, phaseName) {
|
|
144
|
+
const prompt = [
|
|
145
|
+
`You are the M89 ambiguous-claim JUDGE (auto-research-contract §1.1). The mechanical string-fact`,
|
|
146
|
+
`classifier could not decide this GUESSED claim. Decide in natural language. Claim: "${claimText}"`,
|
|
147
|
+
`- "internal" = about THIS repo's OWN code/contracts/tests — grep/Read can confirm.`,
|
|
148
|
+
`- "external" = asserts an OUTSIDE system's behavior/shape/limit — needs web research.`,
|
|
149
|
+
`- "uncertain" = you cannot CONFIDENTLY place it internal — per M89 doctrine it is RESEARCHED, never guessed.`,
|
|
150
|
+
`Return JSON: { "verdict": "internal"|"external"|"uncertain", "reason": "<one line>" }. No file/web work in THIS step.`,
|
|
151
|
+
].join("\n");
|
|
152
|
+
const r = await agent(prompt, { label: "classify-judge", model: "fable", schema: CLASSIFY_JUDGE_SCHEMA, phase: phaseName })
|
|
153
|
+
.catch((e) => ({ verdict: "uncertain", reason: `judge error: ${e && e.message}` }));
|
|
154
|
+
return (r && r.verdict) || "uncertain";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// M89 §2 — research result schema
|
|
158
|
+
const RESEARCH_RESULT_SCHEMA = {
|
|
159
|
+
type: "object", required: ["ok", "gapKey"], additionalProperties: true,
|
|
160
|
+
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" } },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// M89 Research sub-routine: classify+cite one GUESSED claim (§1/§2/§3/§4/§5.1/§7)
|
|
164
|
+
async function runResearchForClaim(projectDir, claimText, artifactPath, phaseName) {
|
|
165
|
+
const claimKey = normalizeClaimKey(claimText);
|
|
166
|
+
|
|
167
|
+
// FAIL-CLOSED (Red Team HIGH): artifactPath is self-reported + optional. An EXTERNAL (or
|
|
168
|
+
// ambiguous→escalated-external) guess MUST get its §7 uncited marker written SOMEWHERE so the
|
|
169
|
+
// §7 ENFORCE gate has something to fail on. Use a DETERMINISTIC FALLBACK ARTIFACT when none.
|
|
170
|
+
const claimSlug = claimKey.replace(/\s+/g, "-").slice(0, 80) || "claim";
|
|
171
|
+
const externalArtifact = artifactPath || `${projectDir}/.gsd-t/research/debug-${claimSlug}.md`;
|
|
172
|
+
|
|
173
|
+
// §4.1 idempotency check — read the path actually WRITTEN (externalArtifact = real OR
|
|
174
|
+
// fallback) so a re-run does not re-research a claim already cited in the fallback (MEDIUM).
|
|
175
|
+
{
|
|
176
|
+
const atxt = await agent(`Read \`${externalArtifact}\` and return JSON: { "text": "<content>" }. If missing: { "text": "" }.`,
|
|
177
|
+
{ label: "read-artifact", model: "haiku", schema: { type: "object", required: ["text"], properties: { text: { type: "string" } }, additionalProperties: false }, phase: phaseName })
|
|
178
|
+
.catch(() => ({ text: "" }));
|
|
179
|
+
if (isAlreadyCited((atxt && atxt.text) || "", claimKey)) {
|
|
180
|
+
log(`Research: skip "${claimKey.slice(0, 50)}" — already cited (§4.1)`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const cls = await classifyClaim(projectDir, claimText, phaseName);
|
|
186
|
+
const envelope = cls.envelope;
|
|
187
|
+
if (!envelope || !envelope.ok) {
|
|
188
|
+
log(`Research: classify error — ${cls.stderr || JSON.stringify(envelope)}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function appendUncitedMarker(ap, key) {
|
|
193
|
+
if (!ap) return;
|
|
194
|
+
const m = uncitedMarker(key);
|
|
195
|
+
const sp = ap.replace(/'/g, "'\\''");
|
|
196
|
+
await agent(
|
|
197
|
+
`Ensure the parent dir exists, then append \`${m}\` to \`${ap}\` if not already present. Bash: \`mkdir -p "$(dirname '${sp}')" && { grep -qF '${m.replace(/'/g, "'\\''")}' '${sp}' 2>/dev/null || echo '${m.replace(/'/g, "'\\''")}' >> '${sp}'; }\`. Return JSON: { "done": true }.`,
|
|
198
|
+
{ label: "write-uncited-marker", model: "haiku", schema: { type: "object", required: ["done"], properties: { done: { type: "boolean" } }, additionalProperties: true }, phase: phaseName }
|
|
199
|
+
).catch(() => {});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function flipAndCite(ap, key, citedBlock) {
|
|
203
|
+
if (!ap || !citedBlock) return;
|
|
204
|
+
const um = uncitedMarker(key);
|
|
205
|
+
const cm = citedMarker(key);
|
|
206
|
+
await agent(
|
|
207
|
+
`Ensure the parent dir of \`${ap}\` exists (Bash: \`mkdir -p "$(dirname '${ap.replace(/'/g, "'\\''")}')"\`). Then Edit \`${ap}\` (create if missing): replace \`${um}\` with \`${cm}\`, then append:\n${citedBlock}\nReturn JSON: { "done": true }.`,
|
|
208
|
+
{ label: "flip-marker-cite", model: "haiku", schema: { type: "object", required: ["done"], properties: { done: { type: "boolean" }, action: { type: "string" } }, additionalProperties: true }, phase: phaseName }
|
|
209
|
+
).catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// External-claim handler closure (reused by the ambiguous→judge path).
|
|
213
|
+
const doExternal = async () => {
|
|
214
|
+
log(`Research: external failure-root → research(fable) instead of patch-guess for "${claimKey.slice(0, 50)}"`);
|
|
215
|
+
await appendUncitedMarker(externalArtifact, claimKey);
|
|
216
|
+
const rr = await agent(
|
|
217
|
+
[
|
|
218
|
+
`Read \`${projectDir}/templates/prompts/research-subagent.md\` for the research protocol.`,
|
|
219
|
+
`Debug failure-root claim (external): "${claimText}"`,
|
|
220
|
+
`Gap-key: "${claimKey}"`,
|
|
221
|
+
`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.`,
|
|
222
|
+
].join("\n"),
|
|
223
|
+
{ label: "research", model: "fable", schema: RESEARCH_RESULT_SCHEMA, phase: phaseName }
|
|
224
|
+
).catch((e) => ({ ok: false, gapKey: claimKey, reason: String(e && e.message) }));
|
|
225
|
+
|
|
226
|
+
if (rr && rr.ok && rr.citedBlock) {
|
|
227
|
+
await flipAndCite(externalArtifact, claimKey, rr.citedBlock);
|
|
228
|
+
log(`Research: cited debug claim "${claimKey.slice(0, 50)}"`);
|
|
229
|
+
} else {
|
|
230
|
+
log(`Research: research failed for "${claimKey.slice(0, 50)}" — uncited marker remains`);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Internal-claim handler closure (grep A3; grep-empty → escalate §5.1).
|
|
235
|
+
const doInternal = async () => {
|
|
236
|
+
log(`Research: internal debug claim → grep for "${claimText.slice(0, 50)}"`);
|
|
237
|
+
const gr = await grepForClaim(projectDir, claimText, phaseName);
|
|
238
|
+
if (gr.found) {
|
|
239
|
+
log(`Research: internal claim resolved by grep — no research needed`);
|
|
240
|
+
} else {
|
|
241
|
+
// §5.1 escalation
|
|
242
|
+
log(`Research: grep empty — escalating to external (§5.1): "${claimText.slice(0, 50)}"`);
|
|
243
|
+
await appendUncitedMarker(externalArtifact, claimKey);
|
|
244
|
+
const er = await agent(
|
|
245
|
+
[
|
|
246
|
+
`Read \`${projectDir}/templates/prompts/research-subagent.md\`.`,
|
|
247
|
+
`Debug escalation (§5.1 — grep returned nothing): "${claimText}"`,
|
|
248
|
+
`Gap-key: "${claimKey}"`,
|
|
249
|
+
`Emit ## Verified Facts (auto-research) block. Append the trailer \`key: ${claimKey}\` on every fact line so the §7 gate matches by claim-key (Red Team MEDIUM #2). Return StructuredOutput JSON.`,
|
|
250
|
+
].join("\n"),
|
|
251
|
+
{ label: "research", model: "fable", schema: RESEARCH_RESULT_SCHEMA, phase: phaseName }
|
|
252
|
+
).catch((e) => ({ ok: false, gapKey: claimKey, reason: String(e && e.message) }));
|
|
253
|
+
|
|
254
|
+
if (er && er.ok && er.citedBlock) {
|
|
255
|
+
await flipAndCite(externalArtifact, claimKey, er.citedBlock);
|
|
256
|
+
log(`Research: escalated debug claim cited "${claimKey.slice(0, 50)}"`);
|
|
257
|
+
} else {
|
|
258
|
+
log(`Research: escalation research failed for "${claimKey.slice(0, 50)}"`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// ── Dispatch by the 3-result classifier verdict (v1.3.0) ────────────────────
|
|
264
|
+
if (envelope.class === "external") {
|
|
265
|
+
await doExternal();
|
|
266
|
+
} else if (envelope.class === "internal") {
|
|
267
|
+
await doInternal();
|
|
268
|
+
} else {
|
|
269
|
+
// class:AMBIGUOUS — no string fact; the LLM judge decides (NOT regex). internal→grep;
|
|
270
|
+
// external→research; UNCERTAIN→research (uncertain = verify, NEVER guess-internal).
|
|
271
|
+
log(`Research: ambiguous debug claim → LLM judge for "${claimText.slice(0, 50)}" (when unsure, research)`);
|
|
272
|
+
const verdict = await judgeAmbiguous(claimText, phaseName);
|
|
273
|
+
log(`Research: ambiguous "${claimKey.slice(0, 50)}" → judge: ${verdict}`);
|
|
274
|
+
if (verdict === "internal") {
|
|
275
|
+
await doInternal();
|
|
276
|
+
} else {
|
|
277
|
+
if (verdict === "uncertain") log(`Research: judge UNCERTAIN → "${claimKey.slice(0, 50)}" treated external → research (no silent guess)`);
|
|
278
|
+
await doExternal();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
74
283
|
if (!symptom) {
|
|
75
284
|
log("debug: args.symptom required (description of failing test or error)");
|
|
76
285
|
return { status: "failed", reason: "no-symptom" };
|
|
@@ -82,6 +291,11 @@ if (!pre.ok) return { status: "failed", reason: "preflight-failed", preflight: p
|
|
|
82
291
|
const brief = await generateBrief(projectDir, { kind: "execute", id: "debug-brief" });
|
|
83
292
|
|
|
84
293
|
let lastResult = null;
|
|
294
|
+
// M90 §3 (fix-cycle: gate lifecycle) — track THIS run's per-signature cycle counts so the
|
|
295
|
+
// non-convergence halt is RUN-SCOPED + REACHABLE within the 2-cycle debug cap (not dependent on
|
|
296
|
+
// the global HALT_THRESHOLD=3 or stale cross-run ledger state). A signature seen in BOTH cycles
|
|
297
|
+
// of THIS run = non-convergence at the cycle-2 boundary (option b).
|
|
298
|
+
const thisRunSigCycles = {}; // { [signature]: maxCyclesSeenThisRun }
|
|
85
299
|
for (let cycle = 1; cycle <= 2; cycle++) {
|
|
86
300
|
phase(`Cycle ${cycle}`);
|
|
87
301
|
const prompt = [
|
|
@@ -93,6 +307,14 @@ for (let cycle = 1; cycle <= 2; cycle++) {
|
|
|
93
307
|
``,
|
|
94
308
|
`Steps: (1) read the relevant code, (2) form a hypothesis, (3) apply a fix, (4) run the affected test(s), (5) report.`,
|
|
95
309
|
`Commit the fix with prefix "m61(debug-cycle${cycle})".`,
|
|
310
|
+
``,
|
|
311
|
+
`**M89 STATED CLAIMS — REQUIRED (auto-research-contract §6.5):**`,
|
|
312
|
+
`Read \`${projectDir}/templates/prompts/stated-claims-snippet.md\` for the DETECT protocol.`,
|
|
313
|
+
`Before returning, emit a \`## Stated Claims\` section tagging every load-bearing claim`,
|
|
314
|
+
`[KNOWN] or [GUESSED:assumed|unknown|stale]. Key: when the failure root involves an external`,
|
|
315
|
+
`API/library behavior you ASSUMED (a guess), tag it GUESSED — the Research stage will`,
|
|
316
|
+
`verify it via web instead of letting you patch-guess. Include "statedClaims" array in`,
|
|
317
|
+
`StructuredOutput JSON: one per [GUESSED:*] line. Include "artifactPath" if relevant.`,
|
|
96
318
|
`Return JSON per the schema.`,
|
|
97
319
|
].filter(Boolean).join("\n");
|
|
98
320
|
|
|
@@ -108,9 +330,99 @@ for (let cycle = 1; cycle <= 2; cycle++) {
|
|
|
108
330
|
nextStepsIfNotResolved: "agent threw — investigate directly",
|
|
109
331
|
}));
|
|
110
332
|
|
|
333
|
+
// M89 Research — classify GUESSED claims from this debug cycle (§6.5 trigger, separate from debug-cycle ternary)
|
|
334
|
+
if (Array.isArray(lastResult.statedClaims) && lastResult.statedClaims.length > 0) {
|
|
335
|
+
for (const claimLine of lastResult.statedClaims) {
|
|
336
|
+
const m = claimLine.match(/^\[GUESSED:[^\]]+\]\s*(.+)$/);
|
|
337
|
+
if (m) {
|
|
338
|
+
await runResearchForClaim(projectDir, m[1].trim(), lastResult.artifactPath || null, `Cycle ${cycle}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
111
343
|
if (lastResult.resolved) {
|
|
112
344
|
return { status: "complete", cyclesUsed: cycle, finalResult: lastResult };
|
|
113
345
|
}
|
|
346
|
+
|
|
347
|
+
// M90 §3 D4-T3 — append-cycle to the loop-ledger (option b: re-anchor halt to cycle-2 boundary).
|
|
348
|
+
// The symptom IS the assertion (the failing test/check); the primary edited file is the surface.
|
|
349
|
+
// The ledger tracks computed signatures cross-process — next cycle's append-cycle accumulates correctly.
|
|
350
|
+
// Uses the same runCli helper (inline agent()-Bash, no require/fs — M81 runtime-native invariant).
|
|
351
|
+
{
|
|
352
|
+
const primarySurface = (Array.isArray(lastResult.filesEdited) && lastResult.filesEdited.length > 0)
|
|
353
|
+
? lastResult.filesEdited[0]
|
|
354
|
+
: "unknown";
|
|
355
|
+
const assertionArg = (symptom || "unknown-symptom").slice(0, 500);
|
|
356
|
+
const surfaceArg = String(primarySurface).slice(0, 200);
|
|
357
|
+
const ledgerAppend = await runCli(
|
|
358
|
+
projectDir,
|
|
359
|
+
"loop-ledger",
|
|
360
|
+
["append-cycle",
|
|
361
|
+
"--assertion", assertionArg,
|
|
362
|
+
"--surface", surfaceArg,
|
|
363
|
+
"--fileClass", "unit",
|
|
364
|
+
...(milestone ? ["--milestone", milestone] : []),
|
|
365
|
+
"--projectDir", projectDir],
|
|
366
|
+
"gsd-t-loop-ledger.cjs",
|
|
367
|
+
`loop-ledger-append-cycle-${cycle}`,
|
|
368
|
+
true,
|
|
369
|
+
`Cycle ${cycle}`
|
|
370
|
+
);
|
|
371
|
+
if (ledgerAppend.envelope) {
|
|
372
|
+
const env = ledgerAppend.envelope;
|
|
373
|
+
log(`M90 loop-ledger cycle ${cycle}: sig=${String(env.signature || "?").slice(0, 16)} cycles=${env.cycles} halted=${env.halted}`);
|
|
374
|
+
// RUN-SCOPED count: increment a LOCAL per-signature counter for each append IN THIS RUN —
|
|
375
|
+
// do NOT read env.cycles (that is the GLOBAL cumulative count across all prior runs; a stale
|
|
376
|
+
// count of 1 from a previous run would make cycle-1 here read 2 and falsely halt). The local
|
|
377
|
+
// count only reaches 2 when the SAME signature is appended in BOTH cycles of THIS run.
|
|
378
|
+
if (env.signature) {
|
|
379
|
+
thisRunSigCycles[env.signature] = (thisRunSigCycles[env.signature] || 0) + 1;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// M90 §3 (option b, fix-cycle: REACHABLE + RUN-SCOPED + AUTO-RESET) — after cycle 2, detect
|
|
386
|
+
// non-convergence from THIS RUN's signatures, not the global HALT_THRESHOLD=3 (which the 2-cycle
|
|
387
|
+
// debug loop can never reach) and not stale cross-run ledger state (which would brick verify).
|
|
388
|
+
// A signature that appeared in BOTH cycles of this run (cycles>=2) IS the cycle-2-boundary halt.
|
|
389
|
+
const nonConvergentSigs = Object.keys(thisRunSigCycles).filter((s) => thisRunSigCycles[s] >= 2);
|
|
390
|
+
if (nonConvergentSigs.length > 0) {
|
|
391
|
+
log("M90 loop-ledger: non-convergence detected — same symptom-signature across both cycles of THIS run.");
|
|
392
|
+
log("PREMISE_RE_EXAMINATION: the fix strategy has not converged. Re-examine the premise, not patch further.");
|
|
393
|
+
// PERSIST the unresolved halt (detection != resolution — user decision, M90 verify fix-cycle 6).
|
|
394
|
+
// Debug DETECTING the loop is NOT the same as the premise being RE-EXAMINED. We MARK the
|
|
395
|
+
// signature as requiring re-examination (halted+pending) and LEAVE IT SET — so the verify
|
|
396
|
+
// R-FAIL-3 gate actually sees the unresolved loop and FAILS, blocking the build until a genuine
|
|
397
|
+
// re-examination runs `record-re-examination`. Auto-clearing here (the prior bug) made the gate
|
|
398
|
+
// vacuous: it could never fire. The brick-risk that motivated auto-clear is handled instead by
|
|
399
|
+
// run-LOCAL counting + the gitignored transient state file (a stale cross-run/cross-project halt
|
|
400
|
+
// can't reach an unrelated verify).
|
|
401
|
+
for (const sig of nonConvergentSigs) {
|
|
402
|
+
await runCli(
|
|
403
|
+
projectDir,
|
|
404
|
+
"loop-ledger",
|
|
405
|
+
["mark-re-examination-required", "--signature", sig, ...(milestone ? ["--milestone", milestone] : []), "--projectDir", projectDir],
|
|
406
|
+
"gsd-t-loop-ledger.cjs",
|
|
407
|
+
"loop-ledger-mark-re-examination-required",
|
|
408
|
+
true,
|
|
409
|
+
"Cycle 2"
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
status: "premise-re-examination",
|
|
414
|
+
reason: "M90 §3 R-LOOP-2: same computed symptom-signature across both debug cycles of this run. " +
|
|
415
|
+
"The fix strategy is non-converging. Per the Unproven-Assumption Doctrine (option b), " +
|
|
416
|
+
"this workflow exits with a PREMISE_RE_EXAMINATION directive instead of dispatching a 3rd patch. " +
|
|
417
|
+
"Action required: stop patching, research how others solved this class of problem, re-examine the premise.",
|
|
418
|
+
directive: "PREMISE_RE_EXAMINATION",
|
|
419
|
+
routeTo: "architectural-hook",
|
|
420
|
+
module: "bin/gsd-t-architectural-trigger.cjs",
|
|
421
|
+
contract: ".gsd-t/contracts/unproven-assumption-doctrine-contract.md §3.2",
|
|
422
|
+
cyclesUsed: 2,
|
|
423
|
+
finalResult: lastResult,
|
|
424
|
+
haltedSignatures: nonConvergentSigs,
|
|
425
|
+
};
|
|
114
426
|
}
|
|
115
427
|
|
|
116
428
|
return {
|