@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
|
@@ -13,6 +13,25 @@
|
|
|
13
13
|
// context-brief, file-disjointness, verify-gate — all invoked from inside
|
|
14
14
|
// stages via templates/workflows/_lib.js helpers.
|
|
15
15
|
//
|
|
16
|
+
// M89: Stated-Claims→classify→research wiring (auto-research-contract v1.2.0 §6.5/§1/§2/§3/§4/§7).
|
|
17
|
+
// Domain workers embed the Stated-Claims snippet (§6.5) so agents tag load-bearing claims
|
|
18
|
+
// KNOWN|GUESSED. After domain workers, the Research phase iterates GUESSED entries through
|
|
19
|
+
// bin/gsd-t-research-gate.cjs: external → write §7 marker (status=uncited) → research agent
|
|
20
|
+
// (model:"fable") → cite → flip marker (status=cited). Internal → grep/Read; grep-empty →
|
|
21
|
+
// escalate to external research + cite + marker (§5.1 ambiguous escalation). Idempotent per §4.1
|
|
22
|
+
// (exact normalized claim-key match only). Wave is a pure composer and does NOT embed this wiring
|
|
23
|
+
// (M85 zero-model: invariant; research reaches wave via its execute sub-workflow).
|
|
24
|
+
//
|
|
25
|
+
// M90 §2 D4-T4: Architectural-trigger wiring (protocol-class path, R-ARCH-2 "everywhere" feed).
|
|
26
|
+
// After domain workers, compute the extend-existing-code signal from each domain's filesTouched:
|
|
27
|
+
// any file that already existed on disk before this execute run is extend-class (the task EXTENDED
|
|
28
|
+
// existing code rather than creating net-new). For each such domain, fire the arch trigger (R-ARCH-2)
|
|
29
|
+
// via the inline runCli helper. This is the NAMED PRODUCER: the signal is computed from real
|
|
30
|
+
// runtime inputs (filesTouched vs. on-disk existence), never from a seeded injection.
|
|
31
|
+
// Result is logged + stored in domainArchTriggerResults for verify gate (R-FAIL-2: if the trigger
|
|
32
|
+
// fires with provenByAdversaryOnly=true and the flag is unresolved, verify FAILS).
|
|
33
|
+
// Contract: unproven-assumption-doctrine-contract.md §2 (R-ARCH-2, protocol-class path).
|
|
34
|
+
//
|
|
16
35
|
// Invocation contract:
|
|
17
36
|
// The Workflow tool reads this file's `meta` block and runs the script
|
|
18
37
|
// body. `args` is the JSON object passed in via Workflow({args}). Expected
|
|
@@ -26,11 +45,12 @@
|
|
|
26
45
|
export const meta = {
|
|
27
46
|
name: "gsd-t-execute",
|
|
28
47
|
description:
|
|
29
|
-
"Run a GSD-T execute phase: preflight → brief → parallel domain workers → integrate barrier → verify-gate",
|
|
48
|
+
"Run a GSD-T execute phase: preflight → brief → parallel domain workers → research (Stated-Claims) → integrate barrier → verify-gate",
|
|
30
49
|
phases: [
|
|
31
50
|
{ title: "Preflight", detail: "gsd-t preflight + brief generation" },
|
|
32
51
|
{ title: "Disjointness", detail: "prove tasks are file-disjoint" },
|
|
33
52
|
{ title: "Domains", detail: "parallel domain workers" },
|
|
53
|
+
{ title: "Research", detail: "classify GUESSED claims + cite external via web research (§6.5/§1/§2/§7)" },
|
|
34
54
|
{ title: "Integrate", detail: "cross-domain wire-up" },
|
|
35
55
|
{ title: "Verify-Gate", detail: "two-track verify-gate" },
|
|
36
56
|
],
|
|
@@ -49,9 +69,107 @@ const DOMAIN_RESULT_SCHEMA = {
|
|
|
49
69
|
tasksDone: { type: "array", items: { type: "string" } },
|
|
50
70
|
tasksBlocked: { type: "array", items: { type: "string" } },
|
|
51
71
|
notes: { type: "string" },
|
|
72
|
+
// M89 §6.5 — Stated Claims: raw [GUESSED:*] lines emitted by the worker (§6.5 DETECT seam).
|
|
73
|
+
// Each entry is the full bullet text e.g. "[GUESSED:assumed] the create call returns a `url`".
|
|
74
|
+
// The Research phase iterates these through bin/gsd-t-research-gate.cjs.
|
|
75
|
+
statedClaims: { type: "array", items: { type: "string" } },
|
|
76
|
+
// Primary artifact path written by this worker (used by Research phase to write/flip §7 markers).
|
|
77
|
+
artifactPath: { type: "string" },
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// M89 §7 — ENFORCE marker format helpers (normalize claim-key per §4.1)
|
|
82
|
+
// Normalize: lowercase, then collapse EVERY non-word run to a single space (cycle-2
|
|
83
|
+
// finding #1) so the key is marker-syntax-safe (no "="/"<"/">"/"-"); byte-identical
|
|
84
|
+
// across all 4 workflows (enforced by m89-normalize-claim-key-parity).
|
|
85
|
+
function normalizeClaimKey(claim) {
|
|
86
|
+
return claim.toLowerCase().replace(/[^\w]+/g, " ").trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// M89 §7 — marker templates
|
|
90
|
+
function uncitedMarker(key) {
|
|
91
|
+
return `<!-- auto-research-claim: class=external key=${key} status=uncited -->`;
|
|
92
|
+
}
|
|
93
|
+
function citedMarker(key) {
|
|
94
|
+
return `<!-- auto-research-claim: class=external key=${key} status=cited -->`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// M89 §2 — research stage schema (StructuredOutput from research-subagent)
|
|
98
|
+
const RESEARCH_RESULT_SCHEMA = {
|
|
99
|
+
type: "object",
|
|
100
|
+
required: ["ok", "gapKey"],
|
|
101
|
+
additionalProperties: true,
|
|
102
|
+
properties: {
|
|
103
|
+
ok: { type: "boolean" },
|
|
104
|
+
gapKey: { type: "string" },
|
|
105
|
+
citedBlock: { type: "string" },
|
|
106
|
+
sourceUrls: { type: "array", items: { type: "string" } },
|
|
107
|
+
fetchDates: { type: "array", items: { type: "string" } },
|
|
108
|
+
reason: { type: "string" },
|
|
52
109
|
},
|
|
53
110
|
};
|
|
54
111
|
|
|
112
|
+
// M89 §1 — classify a guessed claim via bin/gsd-t-research-gate.cjs (D1 envelope)
|
|
113
|
+
async function classifyClaim(projectDir, claimText, phaseName) {
|
|
114
|
+
return runCli(projectDir, "research-gate", ["classify", claimText, "--json"], "gsd-t-research-gate.cjs", "classify-claim", true, phaseName);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// M89 §4.1 — check if a cited marker already exists in artifact text (exact key match)
|
|
118
|
+
function isAlreadyCited(artifactText, claimKey) {
|
|
119
|
+
const needle = `auto-research-claim: class=external key=${claimKey} status=cited`;
|
|
120
|
+
return artifactText.includes(needle);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// M89 §5.1 — grep the repo for an internal claim via an agent Bash call
|
|
124
|
+
async function grepForClaim(projectDir, claimText, phaseName) {
|
|
125
|
+
const escaped = claimText.replace(/'/g, "'\\''").slice(0, 200);
|
|
126
|
+
const prompt = [
|
|
127
|
+
`Decide whether THIS repo's own code/contracts/tests CONFIRM the SPECIFIC claim — not merely`,
|
|
128
|
+
`share vocabulary with it (Red Team MEDIUM). Project: \`${projectDir}\`. Claim: "${escaped}"`,
|
|
129
|
+
`Run: \`grep -r --include="*.js" --include="*.ts" --include="*.md" --include="*.cjs" -l "${escaped.slice(0, 60)}" "${projectDir}" 2>/dev/null | head -5\``,
|
|
130
|
+
`Also try: \`grep -r -l "${escaped.slice(0, 40)}" "${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>", ...] }. Do ONLY this. 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 — the AMBIGUOUS → LLM JUDGE. When the mechanical classifier finds no string
|
|
144
|
+
// fact (class:ambiguous), the LLM decides internal/external/uncertain in natural language.
|
|
145
|
+
// Bare model:"fable" (the research tier — NOT the ?? override form; "judge" is not a
|
|
146
|
+
// designated stage). Returns the verdict string; on any error → "uncertain" (fail toward
|
|
147
|
+
// research — never guess-internal). The caller treats external/uncertain as research.
|
|
148
|
+
const CLASSIFY_JUDGE_SCHEMA = {
|
|
149
|
+
type: "object", required: ["verdict"], additionalProperties: true,
|
|
150
|
+
properties: { verdict: { type: "string", enum: ["internal", "external", "uncertain"] }, reason: { type: "string" } },
|
|
151
|
+
};
|
|
152
|
+
async function judgeAmbiguous(claimText, phaseName) {
|
|
153
|
+
const prompt = [
|
|
154
|
+
`You are the M89 ambiguous-claim JUDGE (auto-research-contract §1.1). The mechanical`,
|
|
155
|
+
`string-fact classifier could not decide this GUESSED claim, so decide it in natural language.`,
|
|
156
|
+
``,
|
|
157
|
+
`Claim: "${claimText}"`,
|
|
158
|
+
``,
|
|
159
|
+
`- "internal" = about THIS repo's OWN code / contracts / schema / tests — grep/Read can confirm.`,
|
|
160
|
+
`- "external" = asserts an OUTSIDE system's behavior/shape/limit (3rd-party API, library, browser,`,
|
|
161
|
+
` protocol, spec) and is unverified — needs web research.`,
|
|
162
|
+
`- "uncertain" = you cannot CONFIDENTLY place it as internal. Use freely — per M89 doctrine an`,
|
|
163
|
+
` unverified claim is RESEARCHED, never guessed.`,
|
|
164
|
+
``,
|
|
165
|
+
`Return JSON: { "verdict": "internal"|"external"|"uncertain", "reason": "<one line>" }.`,
|
|
166
|
+
`Do NOT modify files. Do NOT run web searches in THIS step — only decide the verdict.`,
|
|
167
|
+
].join("\n");
|
|
168
|
+
const r = await agent(prompt, { label: "classify-judge", model: "fable", schema: CLASSIFY_JUDGE_SCHEMA, phase: phaseName })
|
|
169
|
+
.catch((e) => ({ verdict: "uncertain", reason: `judge error: ${e && e.message}` }));
|
|
170
|
+
return (r && r.verdict) || "uncertain";
|
|
171
|
+
}
|
|
172
|
+
|
|
55
173
|
const INTEGRATE_RESULT_SCHEMA = {
|
|
56
174
|
type: "object",
|
|
57
175
|
required: ["status", "crossDomainEdits"],
|
|
@@ -163,6 +281,14 @@ const domainResults = await parallel(
|
|
|
163
281
|
`- Update affected docs (progress.md Decision Log, architecture.md, contracts/, requirements.md, README.md) per the Document Ripple Completion Gate in CLAUDE.md — in the SAME commits as the code changes.`,
|
|
164
282
|
`- If a task is blocked (dependency not met), record it in tasksBlocked and continue with the next.`,
|
|
165
283
|
`- Return a JSON object matching the StructuredOutput schema. The "status" field is the OVERALL domain status: "complete" if all tasks done, "partial" if some done and some blocked, "blocked" if you couldn't start, "failed" on error.`,
|
|
284
|
+
``,
|
|
285
|
+
`**M89 STATED CLAIMS — REQUIRED (auto-research-contract §6.5):**`,
|
|
286
|
+
`Read \`${projectDir}/templates/prompts/stated-claims-snippet.md\` for the full DETECT protocol.`,
|
|
287
|
+
`Before returning your StructuredOutput, emit a \`## Stated Claims\` section listing every`,
|
|
288
|
+
`load-bearing claim you relied on, tagged [KNOWN] or [GUESSED:assumed|unknown|stale].`,
|
|
289
|
+
`In your StructuredOutput JSON, include a "statedClaims" array: one entry per [GUESSED:*] line`,
|
|
290
|
+
`(the full bullet text). Also include "artifactPath" if you wrote a primary output artifact`,
|
|
291
|
+
`(the markdown or file path the Research phase should write §7 markers into).`,
|
|
166
292
|
].join("\n");
|
|
167
293
|
|
|
168
294
|
try {
|
|
@@ -189,6 +315,243 @@ if (blocking.length) {
|
|
|
189
315
|
return { status: "failed", reason: "domain-failed", domainResults };
|
|
190
316
|
}
|
|
191
317
|
|
|
318
|
+
// ── M89 Research Phase — Stated-Claims→classify→cite (§6.5/§1/§2/§3/§4/§7) ──
|
|
319
|
+
phase("Research");
|
|
320
|
+
|
|
321
|
+
// Collect all GUESSED claims from all domain workers (§6.5 DETECT seam)
|
|
322
|
+
const allGuessedClaims = [];
|
|
323
|
+
for (const dr of domainResults.filter(Boolean)) {
|
|
324
|
+
if (Array.isArray(dr.statedClaims) && dr.statedClaims.length > 0) {
|
|
325
|
+
for (const claimLine of dr.statedClaims) {
|
|
326
|
+
// Parse "[GUESSED:type] claim text" → extract the claim text
|
|
327
|
+
const m = claimLine.match(/^\[GUESSED:[^\]]+\]\s*(.+)$/);
|
|
328
|
+
if (m) {
|
|
329
|
+
allGuessedClaims.push({ claimText: m[1].trim(), domain: dr.domain, artifactPath: dr.artifactPath || null, rawLine: claimLine });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (allGuessedClaims.length === 0) {
|
|
336
|
+
log("Research: no GUESSED claims found — skipping classify+research loop");
|
|
337
|
+
} else {
|
|
338
|
+
log(`Research: ${allGuessedClaims.length} GUESSED claim(s) to classify`);
|
|
339
|
+
|
|
340
|
+
// Read artifact text for idempotency check (§4.1 — exact key match)
|
|
341
|
+
// Artifacts are read once per unique path via an agent Bash call
|
|
342
|
+
const artifactCache = {};
|
|
343
|
+
async function readArtifact(artifactPath) {
|
|
344
|
+
if (!artifactPath) return "";
|
|
345
|
+
if (artifactCache[artifactPath] !== undefined) return artifactCache[artifactPath];
|
|
346
|
+
const r = await agent(
|
|
347
|
+
`Read the file at \`${artifactPath}\` and return its full text content in a JSON object: { "text": "<file content>" }. If the file does not exist, return { "text": "" }.`,
|
|
348
|
+
{ label: "read-artifact", model: "haiku", schema: { type: "object", required: ["text"], properties: { text: { type: "string" } }, additionalProperties: false }, phase: "Research" }
|
|
349
|
+
).catch(() => ({ text: "" }));
|
|
350
|
+
const t = (r && r.text) || "";
|
|
351
|
+
artifactCache[artifactPath] = t;
|
|
352
|
+
return t;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Write §7 marker and cited block to artifact via agent Bash (§7 marker lifecycle)
|
|
356
|
+
async function writeMarkerAndCite(artifactPath, claimKey, citedBlock) {
|
|
357
|
+
if (!artifactPath) return;
|
|
358
|
+
const marker = uncitedMarker(claimKey);
|
|
359
|
+
const flipped = citedMarker(claimKey);
|
|
360
|
+
const prompt = [
|
|
361
|
+
`You are performing a precise file edit. Path: \`${artifactPath}\`.`,
|
|
362
|
+
`Do the following in order:`,
|
|
363
|
+
`0. Ensure the parent directory exists (Bash: \`mkdir -p "$(dirname '${artifactPath.replace(/'/g, "'\\''")}')"\`) — the path may be a deterministic fallback artifact that does not exist yet.`,
|
|
364
|
+
`1. Read the file (use the Read tool). If it does not exist, treat its content as empty.`,
|
|
365
|
+
`2. If the file does not contain the line \`${marker}\`, APPEND this block to the end of the file:`,
|
|
366
|
+
` \`\`\``,
|
|
367
|
+
` ${marker}`,
|
|
368
|
+
` ${citedBlock}`,
|
|
369
|
+
` \`\`\``,
|
|
370
|
+
`3. If the file already contains \`${marker}\`, replace it with \`${flipped}\` and also append the cited block if not already present.`,
|
|
371
|
+
`4. If the file already contains \`${flipped}\`, do nothing (already cited — idempotent per §4.1).`,
|
|
372
|
+
`Use the Edit or Write tool to apply the change. Return JSON: { "done": true, "action": "appended|replaced|noop" }.`,
|
|
373
|
+
].join("\n");
|
|
374
|
+
await agent(prompt, { label: "write-marker-cite", model: "haiku", schema: { type: "object", required: ["done"], properties: { done: { type: "boolean" }, action: { type: "string" } }, additionalProperties: true }, phase: "Research" })
|
|
375
|
+
.catch((e) => log(`write-marker-cite error: ${e && e.message}`));
|
|
376
|
+
// Invalidate cache so subsequent reads see the updated file
|
|
377
|
+
delete artifactCache[artifactPath];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function writeUncitedMarker(artifactPath, claimKey) {
|
|
381
|
+
if (!artifactPath) return;
|
|
382
|
+
const marker = uncitedMarker(claimKey);
|
|
383
|
+
const safePath = artifactPath.replace(/'/g, "'\\''");
|
|
384
|
+
const prompt = [
|
|
385
|
+
`Ensure the parent directory exists, then append this line to the file \`${artifactPath}\` (only if not already present — exact string match):`,
|
|
386
|
+
`\`${marker}\``,
|
|
387
|
+
`Use Bash: \`mkdir -p "$(dirname '${safePath}')" && { grep -qF '${marker.replace(/'/g, "'\\''")}' '${safePath}' 2>/dev/null || echo '${marker.replace(/'/g, "'\\''")}' >> '${safePath}'; }\``,
|
|
388
|
+
`Return JSON: { "done": true }.`,
|
|
389
|
+
].join("\n");
|
|
390
|
+
await agent(prompt, { label: "write-uncited-marker", model: "haiku", schema: { type: "object", required: ["done"], properties: { done: { type: "boolean" } }, additionalProperties: true }, phase: "Research" })
|
|
391
|
+
.catch((e) => log(`write-uncited-marker error: ${e && e.message}`));
|
|
392
|
+
delete artifactCache[artifactPath];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Process each GUESSED claim sequentially (classify → route)
|
|
396
|
+
for (const { claimText, domain, artifactPath, rawLine } of allGuessedClaims) {
|
|
397
|
+
const claimKey = normalizeClaimKey(claimText);
|
|
398
|
+
log(`Research: classifying claim "${claimText.slice(0, 60)}" [domain=${domain}]`);
|
|
399
|
+
|
|
400
|
+
// FAIL-CLOSED (Red Team HIGH): artifactPath is worker-self-reported + optional. An EXTERNAL
|
|
401
|
+
// (or ambiguous→escalated-external) guess MUST still get its §7 uncited marker written
|
|
402
|
+
// SOMEWHERE, or the §7 ENFORCE gate finds nothing and the guess ships uncited+unresearched.
|
|
403
|
+
// So the external/escalation path writes to a DETERMINISTIC FALLBACK ARTIFACT when the worker
|
|
404
|
+
// reported no path. Internal/grep can still no-op safely.
|
|
405
|
+
const claimSlug = claimKey.replace(/\s+/g, "-").slice(0, 80) || "claim";
|
|
406
|
+
const externalArtifact = artifactPath || `${projectDir}/.gsd-t/research/${(domain || "domain")}-${claimSlug}.md`;
|
|
407
|
+
|
|
408
|
+
// §4.1 idempotency — skip if already cited (exact key match). Read the path actually
|
|
409
|
+
// WRITTEN (externalArtifact = real path OR the deterministic fallback) so a re-run does
|
|
410
|
+
// NOT re-research a claim already cited in the fallback artifact (Red Team MEDIUM).
|
|
411
|
+
const artifactText = await readArtifact(externalArtifact);
|
|
412
|
+
if (isAlreadyCited(artifactText, claimKey)) {
|
|
413
|
+
log(`Research: skip "${claimKey.slice(0, 50)}" — already cited (§4.1 idempotent)`);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Classify the claim (D1 envelope)
|
|
418
|
+
const cls = await classifyClaim(projectDir, claimText, "Research");
|
|
419
|
+
const envelope = cls.envelope;
|
|
420
|
+
if (!envelope || !envelope.ok) {
|
|
421
|
+
log(`Research: classify error for "${claimText.slice(0, 50)}" — ${cls.stderr || JSON.stringify(envelope)}`);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// External-claim handler (§7 marker → research(fable) → cite → flip). Closure so the
|
|
426
|
+
// ambiguous→judge path reuses it for an "external"/"uncertain" verdict.
|
|
427
|
+
const doExternal = async () => {
|
|
428
|
+
log(`Research: external claim → write §7 marker + fable research for "${claimKey.slice(0, 50)}"${artifactPath ? "" : " (FALLBACK artifact — worker reported no path)"}`);
|
|
429
|
+
// §7: write uncited marker at classify time (to the real OR fallback artifact — fail-CLOSED)
|
|
430
|
+
await writeUncitedMarker(externalArtifact, claimKey);
|
|
431
|
+
|
|
432
|
+
// §2: research agent — bare "fable" tier literal (NOT the ??-override form; contract §2)
|
|
433
|
+
const researchPrompt = [
|
|
434
|
+
`Read \`${projectDir}/templates/prompts/research-subagent.md\` for the full research protocol.`,
|
|
435
|
+
`Your task: verify this external guessed claim via live web sources.`,
|
|
436
|
+
`Claim: "${claimText}"`,
|
|
437
|
+
`Gap-key (normalized): "${claimKey}"`,
|
|
438
|
+
`Emit a ## Verified Facts (auto-research) block with source URL + fetch date per the protocol. Append the trailer \`key: ${claimKey}\` on every fact line so the §7 gate matches by claim-key (Red Team MEDIUM #2).`,
|
|
439
|
+
`Return StructuredOutput JSON per the schema.`,
|
|
440
|
+
].join("\n");
|
|
441
|
+
const researchResult = await agent(researchPrompt, {
|
|
442
|
+
label: "research",
|
|
443
|
+
model: "fable",
|
|
444
|
+
schema: RESEARCH_RESULT_SCHEMA,
|
|
445
|
+
phase: "Research",
|
|
446
|
+
}).catch((e) => ({ ok: false, gapKey: claimKey, reason: `research agent error: ${e && e.message}` }));
|
|
447
|
+
|
|
448
|
+
if (researchResult && researchResult.ok && researchResult.citedBlock) {
|
|
449
|
+
// §7: flip marker to cited + write cited block (real OR fallback artifact)
|
|
450
|
+
await writeMarkerAndCite(externalArtifact, claimKey, researchResult.citedBlock);
|
|
451
|
+
log(`Research: cited "${claimKey.slice(0, 50)}" — marker flipped to status=cited`);
|
|
452
|
+
} else {
|
|
453
|
+
log(`Research: research FAILED for "${claimKey.slice(0, 50)}" — marker stays uncited. Reason: ${(researchResult && researchResult.reason) || "unknown"}`);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// Internal-claim handler — grep/Read (A3); grep-empty → escalate to external (§5.1).
|
|
458
|
+
const doInternal = async () => {
|
|
459
|
+
log(`Research: internal claim → grep/Read for "${claimText.slice(0, 50)}"`);
|
|
460
|
+
const grepResult = await grepForClaim(projectDir, claimText, "Research");
|
|
461
|
+
if (grepResult.found) {
|
|
462
|
+
log(`Research: internal claim resolved by grep (${(grepResult.matches || []).join(", ").slice(0, 80)}) — no research needed`);
|
|
463
|
+
// No marker, no research stage (A3 — internal class never enters research)
|
|
464
|
+
} else {
|
|
465
|
+
// §5.1 escalation: grep-empty → escalate to external. Reuse doExternal() (same
|
|
466
|
+
// marker→research→cite→flip path, incl. the key: trailer) instead of duplicating it.
|
|
467
|
+
log(`Research: grep returned nothing — escalating ambiguous claim to external (§5.1): "${claimText.slice(0, 50)}"`);
|
|
468
|
+
await doExternal();
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// ── Dispatch by the 3-result classifier verdict (v1.3.0) ──────────────────
|
|
473
|
+
if (envelope.class === "external") {
|
|
474
|
+
await doExternal();
|
|
475
|
+
} else if (envelope.class === "internal") {
|
|
476
|
+
await doInternal();
|
|
477
|
+
} else {
|
|
478
|
+
// class:AMBIGUOUS — no string fact; the LLM judge decides (NOT regex). internal→grep;
|
|
479
|
+
// external→research; UNCERTAIN→research (uncertain = verify, NEVER guess-internal).
|
|
480
|
+
log(`Research: ambiguous claim → LLM judge for "${claimText.slice(0, 50)}" (no string fact; when unsure, research)`);
|
|
481
|
+
const verdict = await judgeAmbiguous(claimText, "Research");
|
|
482
|
+
log(`Research: ambiguous claim "${claimKey.slice(0, 50)}" → judge verdict: ${verdict}`);
|
|
483
|
+
if (verdict === "internal") {
|
|
484
|
+
await doInternal();
|
|
485
|
+
} else {
|
|
486
|
+
if (verdict === "uncertain") log(`Research: judge UNCERTAIN → treating "${claimKey.slice(0, 50)}" as external → research (no silent guess)`);
|
|
487
|
+
await doExternal();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── M90 §2 D4-T4 — Arch-Trigger (protocol-class, extend-existing-code signal) ──
|
|
494
|
+
// For each domain, compute the extend-existing-code signal: any file in filesTouched that
|
|
495
|
+
// already existed on disk before this execute run → extend-class → fire the arch trigger.
|
|
496
|
+
// PRODUCER: signal is computed from real runtime inputs (filesTouched vs. on-disk check).
|
|
497
|
+
// The trigger is best-effort (non-blocking): log results for verify gate; don't halt execute.
|
|
498
|
+
const domainArchTriggerResults = [];
|
|
499
|
+
for (const dr of domainResults.filter(Boolean)) {
|
|
500
|
+
if (!Array.isArray(dr.filesTouched) || dr.filesTouched.length === 0) continue;
|
|
501
|
+
// Check which files existed on disk before this run (extend-class signal).
|
|
502
|
+
// agent()-Bash: check existence via `test -f <file>` for each touched file.
|
|
503
|
+
const touchedFiles = dr.filesTouched.slice(0, 10); // cap at 10 per domain
|
|
504
|
+
const checkResult = await agent(
|
|
505
|
+
[
|
|
506
|
+
`For the project at \`${projectDir}\`, check which of these files existed on disk BEFORE the current execute run.`,
|
|
507
|
+
`Files to check: ${JSON.stringify(touchedFiles)}`,
|
|
508
|
+
`For each file, run: \`test -f '${projectDir}/<file>' && echo EXISTS || echo ABSENT\` (substitute <file> with the actual path).`,
|
|
509
|
+
`Return JSON: { "existingFiles": ["<file>", ...] } — list ONLY files that exist (exit 0 from test -f).`,
|
|
510
|
+
`Do NOT modify any files. Read-only existence check.`,
|
|
511
|
+
].join("\n"),
|
|
512
|
+
{
|
|
513
|
+
label: `arch-trigger-exist-check:${dr.domain}`,
|
|
514
|
+
phase: "Research",
|
|
515
|
+
model: "haiku",
|
|
516
|
+
schema: {
|
|
517
|
+
type: "object", required: ["existingFiles"],
|
|
518
|
+
properties: { existingFiles: { type: "array", items: { type: "string" } } },
|
|
519
|
+
additionalProperties: true,
|
|
520
|
+
},
|
|
521
|
+
}
|
|
522
|
+
).catch(() => ({ existingFiles: [] }));
|
|
523
|
+
|
|
524
|
+
const existingFiles = Array.isArray(checkResult && checkResult.existingFiles) ? checkResult.existingFiles : [];
|
|
525
|
+
if (existingFiles.length === 0) continue; // no extend-class signal for this domain
|
|
526
|
+
|
|
527
|
+
// Signal confirmed: domain extended existing code → fire arch trigger (R-ARCH-2)
|
|
528
|
+
const triggerInput = JSON.stringify({
|
|
529
|
+
type: "extend-existing-code",
|
|
530
|
+
context: `domain:${dr.domain} extended existing files: ${existingFiles.slice(0, 3).join(", ")}`,
|
|
531
|
+
basis: `Execute domain ${dr.domain} edited existing file(s): ${existingFiles.slice(0, 3).join(", ")}`,
|
|
532
|
+
});
|
|
533
|
+
const triggerResult = await runCli(
|
|
534
|
+
projectDir,
|
|
535
|
+
"architectural-trigger",
|
|
536
|
+
["trigger", triggerInput],
|
|
537
|
+
"gsd-t-architectural-trigger.cjs",
|
|
538
|
+
`arch-trigger:${dr.domain}`,
|
|
539
|
+
true,
|
|
540
|
+
"Research"
|
|
541
|
+
);
|
|
542
|
+
const triggerEnv = triggerResult.envelope || {};
|
|
543
|
+
log(`M90 arch-trigger ${dr.domain}: fired=${triggerEnv.fired} reason=${triggerEnv.reason || "?"} provenByAdversaryOnly=${triggerEnv.provenByAdversaryOnly || false}`);
|
|
544
|
+
domainArchTriggerResults.push({
|
|
545
|
+
domain: dr.domain,
|
|
546
|
+
existingFiles,
|
|
547
|
+
fired: triggerEnv.fired || false,
|
|
548
|
+
reason: triggerEnv.reason || null,
|
|
549
|
+
provenByAdversaryOnly: triggerEnv.provenByAdversaryOnly || false,
|
|
550
|
+
stopDirective: triggerEnv.stopDirective || false,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
log(`M90 arch-trigger: ${domainArchTriggerResults.length} domain(s) fired extend-existing-code signal`);
|
|
554
|
+
|
|
192
555
|
phase("Integrate");
|
|
193
556
|
const integratePrompt = [
|
|
194
557
|
`You are the integration agent. ${domainResults.length} domain workers have completed.`,
|
|
@@ -228,4 +591,6 @@ return {
|
|
|
228
591
|
domainResults,
|
|
229
592
|
integrate,
|
|
230
593
|
verifyGate: vg.envelope,
|
|
594
|
+
// M90 §2: arch-trigger results (surfaced for verify gate R-FAIL-2 check)
|
|
595
|
+
archTriggerResults: domainArchTriggerResults,
|
|
231
596
|
};
|