@tekyzinc/gsd-t 3.23.11 → 3.25.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 +48 -0
- package/README.md +7 -0
- package/bin/cli-preflight-checks/branch-guard.cjs +110 -0
- package/bin/cli-preflight-checks/contracts-stable.cjs +128 -0
- package/bin/cli-preflight-checks/deps-installed.cjs +89 -0
- package/bin/cli-preflight-checks/manifest-fresh.cjs +98 -0
- package/bin/cli-preflight-checks/ports-free.cjs +110 -0
- package/bin/cli-preflight-checks/working-tree-state.cjs +149 -0
- package/bin/cli-preflight.cjs +265 -0
- package/bin/gsd-t-context-brief-kinds/design-verify.cjs +139 -0
- package/bin/gsd-t-context-brief-kinds/execute.cjs +205 -0
- package/bin/gsd-t-context-brief-kinds/qa.cjs +130 -0
- package/bin/gsd-t-context-brief-kinds/red-team.cjs +131 -0
- package/bin/gsd-t-context-brief-kinds/scan.cjs +118 -0
- package/bin/gsd-t-context-brief-kinds/verify.cjs +157 -0
- package/bin/gsd-t-context-brief.cjs +395 -0
- package/bin/gsd-t-ratelimit-probe-worker.cjs +236 -0
- package/bin/gsd-t-ratelimit-probe.cjs +648 -0
- package/bin/gsd-t-verify-gate-judge.cjs +224 -0
- package/bin/gsd-t-verify-gate.cjs +612 -0
- package/bin/gsd-t.js +45 -1
- package/bin/live-activity-report.cjs +615 -0
- package/bin/m55-substrate-proof.cjs +134 -0
- package/bin/parallel-cli-tee.cjs +206 -0
- package/bin/parallel-cli.cjs +478 -0
- package/commands/gsd-t-execute.md +31 -0
- package/commands/gsd-t-help.md +21 -0
- package/commands/gsd-t-verify.md +38 -0
- package/docs/architecture.md +194 -0
- package/docs/diagrams/.gsd-t/.context-meter-state.json +10 -0
- package/docs/diagrams/.gsd-t/context-meter.log +9 -0
- package/docs/diagrams/.gsd-t/events/2026-05-08.jsonl +45 -0
- package/docs/diagrams/.gsd-t/events/2026-05-09.jsonl +1 -0
- package/docs/diagrams/.gsd-t/heartbeat-cd9e7f59-ba5b-406a-9ed6-16762f039e81.jsonl +48 -0
- package/docs/diagrams/01-top-level-map-d2.png +0 -0
- package/docs/diagrams/01-top-level-map.d2 +77 -0
- package/docs/diagrams/01-top-level-map.mmd +48 -0
- package/docs/diagrams/01-top-level-map.png +0 -0
- package/docs/diagrams/01-top-level-map.svg +126 -0
- package/docs/diagrams/02-milestone-lifecycle-d2.png +0 -0
- package/docs/diagrams/02-milestone-lifecycle.d2 +38 -0
- package/docs/diagrams/02-milestone-lifecycle.mmd +36 -0
- package/docs/diagrams/02-milestone-lifecycle.png +0 -0
- package/docs/diagrams/02-milestone-lifecycle.svg +114 -0
- package/docs/diagrams/03-wave-mode-d2.png +0 -0
- package/docs/diagrams/03-wave-mode.d2 +33 -0
- package/docs/diagrams/03-wave-mode.mmd +21 -0
- package/docs/diagrams/03-wave-mode.png +0 -0
- package/docs/diagrams/03-wave-mode.svg +113 -0
- package/docs/diagrams/04-design-to-code-d2.png +0 -0
- package/docs/diagrams/04-design-to-code.d2 +35 -0
- package/docs/diagrams/04-design-to-code.mmd +29 -0
- package/docs/diagrams/04-design-to-code.png +0 -0
- package/docs/diagrams/04-design-to-code.svg +115 -0
- package/docs/diagrams/05-backlog-d2.png +0 -0
- package/docs/diagrams/05-backlog.d2 +40 -0
- package/docs/diagrams/05-backlog.mmd +20 -0
- package/docs/diagrams/05-backlog.png +0 -0
- package/docs/diagrams/05-backlog.svg +113 -0
- package/docs/diagrams/06-automation-utilities-d2.png +0 -0
- package/docs/diagrams/06-automation-utilities.d2 +48 -0
- package/docs/diagrams/06-automation-utilities.mmd +47 -0
- package/docs/diagrams/06-automation-utilities.png +0 -0
- package/docs/diagrams/06-automation-utilities.svg +110 -0
- package/docs/diagrams/_theme.d2 +86 -0
- package/docs/requirements.md +48 -0
- package/docs/workflow-diagram.md +338 -0
- package/package.json +1 -1
- package/scripts/gsd-t-dashboard-server.js +190 -0
- package/scripts/gsd-t-transcript.html +200 -0
- package/templates/CLAUDE-global.md +46 -0
- package/templates/prompts/design-verify-subagent.md +3 -0
- package/templates/prompts/qa-subagent.md +3 -0
- package/templates/prompts/red-team-subagent.md +3 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* execute kind collector — pulls domain scope/constraints/tasks/contracts
|
|
5
|
+
* for a single domain so an execute-phase worker can grep the brief
|
|
6
|
+
* instead of re-walking the repo.
|
|
7
|
+
*
|
|
8
|
+
* Fail-open: if the domain dir is missing, scope/constraints/contracts
|
|
9
|
+
* are empty arrays — the brief is still written.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const NAME = 'execute';
|
|
16
|
+
|
|
17
|
+
function _readMaybe(file) {
|
|
18
|
+
try { return fs.readFileSync(file, 'utf8'); } catch (_) { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract bullet items under a `## Heading` (non-greedy, until the next `## ` or EOF).
|
|
23
|
+
*/
|
|
24
|
+
function _bulletsUnderSection(text, headingPattern) {
|
|
25
|
+
if (!text) return [];
|
|
26
|
+
// Locate `## <heading>` line; capture body until next `## ` or end-of-string.
|
|
27
|
+
// (JS regex has no `\Z`; use the m-flag $ + a non-greedy lookahead, then
|
|
28
|
+
// trim the tail at the next H2.)
|
|
29
|
+
const re = new RegExp('^##\\s+' + headingPattern + '\\s*$', 'mi');
|
|
30
|
+
const m = text.match(re);
|
|
31
|
+
if (!m) return [];
|
|
32
|
+
const start = m.index + m[0].length;
|
|
33
|
+
const remainder = text.slice(start);
|
|
34
|
+
const next = remainder.match(/^##\s+/m);
|
|
35
|
+
const body = next ? remainder.slice(0, next.index) : remainder;
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const line of body.split(/\r?\n/)) {
|
|
38
|
+
// Only top-level bullets (no leading whitespace) — sub-bullets belong to
|
|
39
|
+
// their parent and would inflate the brief size.
|
|
40
|
+
const bm = line.match(/^[-*]\s+(.+)$/);
|
|
41
|
+
if (!bm) continue;
|
|
42
|
+
let item = bm[1].replace(/`/g, '').replace(/\*\*/g, '').trim();
|
|
43
|
+
if (!item) continue;
|
|
44
|
+
if (item.length > 240) item = item.slice(0, 237) + '...';
|
|
45
|
+
out.push(item);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract a path glob list from "Owned Files/Directories" section bullets,
|
|
52
|
+
* keeping only the leading code-fence path before any em-dash.
|
|
53
|
+
*/
|
|
54
|
+
function _sectionBody(text, headingRe) {
|
|
55
|
+
if (!text) return '';
|
|
56
|
+
const m = text.match(headingRe);
|
|
57
|
+
if (!m) return '';
|
|
58
|
+
const start = m.index + m[0].length;
|
|
59
|
+
const remainder = text.slice(start);
|
|
60
|
+
const next = remainder.match(/^##\s+/m);
|
|
61
|
+
return next ? remainder.slice(0, next.index) : remainder;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function _ownedPathsFromScope(text) {
|
|
65
|
+
const body = _sectionBody(text, /^##\s+Owned Files\/Directories\s*$/mi);
|
|
66
|
+
if (!body) return [];
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const line of body.split(/\r?\n/)) {
|
|
69
|
+
// Top-level only — captures `- ` `path``... but skips ` - sub.cjs ...` two-space-indent sub-bullets.
|
|
70
|
+
const bm = line.match(/^[-*]\s+`([^`]+)`/);
|
|
71
|
+
if (bm) out.push(bm[1]);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _notOwnedPaths(text) {
|
|
77
|
+
const body = _sectionBody(text, /^##\s+NOT Owned[^\n]*$/mi);
|
|
78
|
+
if (!body) return [];
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const line of body.split(/\r?\n/)) {
|
|
81
|
+
const bm = line.match(/^[-*]\s+`([^`]+)`/);
|
|
82
|
+
if (bm) out.push(bm[1]);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Collect contract paths referenced from inside the scope text — pattern
|
|
89
|
+
* `.gsd-t/contracts/<name>.md`.
|
|
90
|
+
*/
|
|
91
|
+
function _contractsReferenced(text, projectDir) {
|
|
92
|
+
if (!text) return [];
|
|
93
|
+
const re = /\.gsd-t\/contracts\/[a-zA-Z0-9_./-]+\.md/g;
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
const out = [];
|
|
96
|
+
let m;
|
|
97
|
+
while ((m = re.exec(text)) != null) {
|
|
98
|
+
const p = m[0];
|
|
99
|
+
if (seen.has(p)) continue;
|
|
100
|
+
seen.add(p);
|
|
101
|
+
let status = 'UNKNOWN';
|
|
102
|
+
const full = path.join(projectDir, p);
|
|
103
|
+
try {
|
|
104
|
+
const c = fs.readFileSync(full, 'utf8');
|
|
105
|
+
const sm = c.match(/Status:\s*\**\s*(STABLE|DRAFT|PROPOSED)/i);
|
|
106
|
+
if (sm) status = sm[1].toUpperCase();
|
|
107
|
+
} catch (_) { /* fail-open */ }
|
|
108
|
+
out.push({ path: p, status });
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract the task ids ("T1", "T-2", "M55-D4-T3", …) from tasks.md.
|
|
115
|
+
*/
|
|
116
|
+
function _tasksFromTasksMd(text) {
|
|
117
|
+
if (!text) return [];
|
|
118
|
+
const out = [];
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
function push(id) { if (!seen.has(id)) { seen.add(id); out.push(id); } }
|
|
121
|
+
|
|
122
|
+
// ##/### M{NN}-D{N}-T{N} (Shape C heading) — try BEFORE the bare-T regex so
|
|
123
|
+
// the M-prefixed id wins.
|
|
124
|
+
const reC = /^#{2,3}\s+(M\d+-D\d+-T\d+)\b[^\n]*$/gm;
|
|
125
|
+
let m;
|
|
126
|
+
while ((m = reC.exec(text)) != null) push(m[1]);
|
|
127
|
+
|
|
128
|
+
// ## T1 — heading (Shape A — bare T-id)
|
|
129
|
+
const reH = /^##\s+(T-?\d+)\b[^\n]*$/gm;
|
|
130
|
+
while ((m = reH.exec(text)) != null) push(m[1]);
|
|
131
|
+
|
|
132
|
+
// - [ ] **M55-D4-T1** (Shape C bullets)
|
|
133
|
+
const reBC = /^\s*-\s*\[[ x]\]\s+\*\*(M\d+-D\d+-T\d+)\*\*/gm;
|
|
134
|
+
while ((m = reBC.exec(text)) != null) push(m[1]);
|
|
135
|
+
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function collect(ctx) {
|
|
140
|
+
const { projectDir, domain, recordSource } = ctx;
|
|
141
|
+
const ancillary = {
|
|
142
|
+
expectedOutputs: [],
|
|
143
|
+
filesOwned: [],
|
|
144
|
+
notOwned: [],
|
|
145
|
+
tasks: [],
|
|
146
|
+
};
|
|
147
|
+
let scope = { owned: [], notOwned: [], deliverables: [] };
|
|
148
|
+
let constraints = [];
|
|
149
|
+
let contracts = [];
|
|
150
|
+
|
|
151
|
+
if (!domain) {
|
|
152
|
+
return { scope, constraints, contracts, ancillary };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const dir = path.join(projectDir, '.gsd-t', 'domains', domain);
|
|
156
|
+
if (!fs.existsSync(dir)) {
|
|
157
|
+
// Fail-open: domain dir absent → empty fields.
|
|
158
|
+
return { scope, constraints, contracts, ancillary };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const scopePath = '.gsd-t/domains/' + domain + '/scope.md';
|
|
162
|
+
const constraintsPath = '.gsd-t/domains/' + domain + '/constraints.md';
|
|
163
|
+
const tasksPath = '.gsd-t/domains/' + domain + '/tasks.md';
|
|
164
|
+
|
|
165
|
+
const scopeText = _readMaybe(path.join(projectDir, scopePath));
|
|
166
|
+
if (scopeText) recordSource(scopePath);
|
|
167
|
+
const constraintsText = _readMaybe(path.join(projectDir, constraintsPath));
|
|
168
|
+
if (constraintsText) recordSource(constraintsPath);
|
|
169
|
+
const tasksText = _readMaybe(path.join(projectDir, tasksPath));
|
|
170
|
+
if (tasksText) recordSource(tasksPath);
|
|
171
|
+
|
|
172
|
+
const owned = _ownedPathsFromScope(scopeText);
|
|
173
|
+
const notOwned = _notOwnedPaths(scopeText);
|
|
174
|
+
const deliverables = _bulletsUnderSection(scopeText, 'Deliverables');
|
|
175
|
+
|
|
176
|
+
scope = { owned, notOwned, deliverables };
|
|
177
|
+
|
|
178
|
+
const mustFollow = _bulletsUnderSection(constraintsText, 'Must Follow');
|
|
179
|
+
const mustNot = _bulletsUnderSection(constraintsText, 'Must Not');
|
|
180
|
+
// Mark items by prefix so the worker can tell them apart.
|
|
181
|
+
constraints = mustFollow.map((c) => 'MUST: ' + c).concat(mustNot.map((c) => 'MUST NOT: ' + c));
|
|
182
|
+
|
|
183
|
+
contracts = _contractsReferenced(scopeText, projectDir);
|
|
184
|
+
|
|
185
|
+
const tasks = _tasksFromTasksMd(tasksText);
|
|
186
|
+
|
|
187
|
+
ancillary.filesOwned = owned;
|
|
188
|
+
ancillary.notOwned = notOwned;
|
|
189
|
+
ancillary.expectedOutputs = deliverables;
|
|
190
|
+
ancillary.tasks = tasks;
|
|
191
|
+
|
|
192
|
+
return { scope, constraints, contracts, ancillary };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
name: NAME,
|
|
197
|
+
requiresSources: [],
|
|
198
|
+
collect,
|
|
199
|
+
// Test-only exports
|
|
200
|
+
_bulletsUnderSection,
|
|
201
|
+
_ownedPathsFromScope,
|
|
202
|
+
_notOwnedPaths,
|
|
203
|
+
_contractsReferenced,
|
|
204
|
+
_tasksFromTasksMd,
|
|
205
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* qa kind collector — points the QA subagent at its protocol path,
|
|
5
|
+
* detects the project test runner, and surfaces the most recent
|
|
6
|
+
* qa-issues entries.
|
|
7
|
+
*
|
|
8
|
+
* Fail-CLOSED: requires `templates/prompts/qa-subagent.md`. Library
|
|
9
|
+
* enforces this via the `requiresSources` declaration.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const NAME = 'qa';
|
|
16
|
+
const PROTOCOL = 'templates/prompts/qa-subagent.md';
|
|
17
|
+
const QA_ISSUES = '.gsd-t/qa-issues.md';
|
|
18
|
+
const TAIL_LIMIT = 10;
|
|
19
|
+
|
|
20
|
+
function _readMaybe(file) {
|
|
21
|
+
try { return fs.readFileSync(file, 'utf8'); } catch (_) { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _detectTestRunner(projectDir) {
|
|
25
|
+
const detected = {
|
|
26
|
+
npmTest: null,
|
|
27
|
+
playwrightConfig: null,
|
|
28
|
+
cypressConfig: null,
|
|
29
|
+
jestConfig: null,
|
|
30
|
+
vitestConfig: null,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
34
|
+
try {
|
|
35
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
36
|
+
if (pkg && pkg.scripts && typeof pkg.scripts.test === 'string') {
|
|
37
|
+
detected.npmTest = pkg.scripts.test;
|
|
38
|
+
}
|
|
39
|
+
} catch (_) { /* fail-open */ }
|
|
40
|
+
|
|
41
|
+
for (const f of ['playwright.config.ts', 'playwright.config.js', 'playwright.config.cjs', 'playwright.config.mjs']) {
|
|
42
|
+
if (fs.existsSync(path.join(projectDir, f))) { detected.playwrightConfig = f; break; }
|
|
43
|
+
}
|
|
44
|
+
for (const f of ['cypress.config.ts', 'cypress.config.js', 'cypress.config.cjs']) {
|
|
45
|
+
if (fs.existsSync(path.join(projectDir, f))) { detected.cypressConfig = f; break; }
|
|
46
|
+
}
|
|
47
|
+
for (const f of ['jest.config.ts', 'jest.config.js', 'jest.config.cjs']) {
|
|
48
|
+
if (fs.existsSync(path.join(projectDir, f))) { detected.jestConfig = f; break; }
|
|
49
|
+
}
|
|
50
|
+
for (const f of ['vitest.config.ts', 'vitest.config.js', 'vitest.config.cjs', 'vitest.config.mjs']) {
|
|
51
|
+
if (fs.existsSync(path.join(projectDir, f))) { detected.vitestConfig = f; break; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return detected;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _tailQaIssues(text) {
|
|
58
|
+
if (!text) return [];
|
|
59
|
+
const lines = text.split(/\r?\n/);
|
|
60
|
+
// qa-issues.md is generally a markdown table; keep last N table rows
|
|
61
|
+
// (rows beginning with `|` and not separator-only).
|
|
62
|
+
const rows = lines.filter((l) => /^\|/.test(l) && !/^\|\s*-+\s*\|/.test(l));
|
|
63
|
+
// Drop header (first row), then keep tail.
|
|
64
|
+
const body = rows.slice(1);
|
|
65
|
+
const tail = body.slice(-TAIL_LIMIT).map((l) => {
|
|
66
|
+
const t = l.trim();
|
|
67
|
+
return t.length > 200 ? t.slice(0, 197) + '...' : t;
|
|
68
|
+
});
|
|
69
|
+
return tail;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _scanContracts(projectDir) {
|
|
73
|
+
// Pure scan; caller decides which subset to record as sources.
|
|
74
|
+
const dir = path.join(projectDir, '.gsd-t', 'contracts');
|
|
75
|
+
let entries;
|
|
76
|
+
try { entries = fs.readdirSync(dir); } catch (_) { return []; }
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const f of entries) {
|
|
79
|
+
if (!f.endsWith('.md')) continue;
|
|
80
|
+
const rel = '.gsd-t/contracts/' + f;
|
|
81
|
+
const text = _readMaybe(path.join(projectDir, rel));
|
|
82
|
+
if (!text) continue;
|
|
83
|
+
const m = text.match(/Status:\s*\**\s*(STABLE|DRAFT|PROPOSED)/i);
|
|
84
|
+
out.push({ path: rel, status: m ? m[1].toUpperCase() : 'UNKNOWN' });
|
|
85
|
+
}
|
|
86
|
+
out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function collect(ctx) {
|
|
91
|
+
const { projectDir, recordSource } = ctx;
|
|
92
|
+
|
|
93
|
+
recordSource(PROTOCOL);
|
|
94
|
+
recordSource('package.json');
|
|
95
|
+
|
|
96
|
+
const issuesText = _readMaybe(path.join(projectDir, QA_ISSUES));
|
|
97
|
+
if (issuesText != null) recordSource(QA_ISSUES);
|
|
98
|
+
|
|
99
|
+
const runner = _detectTestRunner(projectDir);
|
|
100
|
+
const recentIssues = _tailQaIssues(issuesText);
|
|
101
|
+
const allContracts = _scanContracts(projectDir);
|
|
102
|
+
// Only DRAFT / PROPOSED contracts are surfaced individually — QA's job is
|
|
103
|
+
// to verify implementations match contracts, but a 65-contract list would
|
|
104
|
+
// bust the 10 KB cap. STABLE / UNKNOWN are counted, not enumerated.
|
|
105
|
+
const enumerated = allContracts.filter((c) => c.status === 'DRAFT' || c.status === 'PROPOSED');
|
|
106
|
+
for (const c of enumerated) recordSource(c.path);
|
|
107
|
+
const counts = { STABLE: 0, DRAFT: 0, PROPOSED: 0, UNKNOWN: 0 };
|
|
108
|
+
for (const c of allContracts) counts[c.status] = (counts[c.status] || 0) + 1;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
scope: { owned: [], notOwned: [], deliverables: [] },
|
|
112
|
+
constraints: [],
|
|
113
|
+
contracts: enumerated,
|
|
114
|
+
ancillary: {
|
|
115
|
+
contractCount: allContracts.length,
|
|
116
|
+
contractStatusCounts: counts,
|
|
117
|
+
protocolPath: PROTOCOL,
|
|
118
|
+
qaIssuesTail: recentIssues,
|
|
119
|
+
testRunner: runner,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
name: NAME,
|
|
126
|
+
requiresSources: [PROTOCOL],
|
|
127
|
+
collect,
|
|
128
|
+
_detectTestRunner,
|
|
129
|
+
_tailQaIssues,
|
|
130
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* red-team kind collector — points the Red Team subagent at its protocol,
|
|
5
|
+
* surfaces the recent commits in scope, and seeds attack vector hints
|
|
6
|
+
* extracted from the protocol's "broken patches" section.
|
|
7
|
+
*
|
|
8
|
+
* Fail-CLOSED: requires `templates/prompts/red-team-subagent.md`. Library
|
|
9
|
+
* enforces this via the `requiresSources` declaration.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const NAME = 'red-team';
|
|
17
|
+
const PROTOCOL = 'templates/prompts/red-team-subagent.md';
|
|
18
|
+
const COMMIT_LIMIT = 10;
|
|
19
|
+
|
|
20
|
+
function _readMaybe(file) {
|
|
21
|
+
try { return fs.readFileSync(file, 'utf8'); } catch (_) { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _recentCommits(projectDir) {
|
|
25
|
+
try {
|
|
26
|
+
const stdout = execSync('git log -' + COMMIT_LIMIT + ' --oneline --no-color', {
|
|
27
|
+
cwd: projectDir,
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
30
|
+
});
|
|
31
|
+
return String(stdout || '')
|
|
32
|
+
.split(/\r?\n/)
|
|
33
|
+
.map((l) => l.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.slice(0, COMMIT_LIMIT);
|
|
36
|
+
} catch (_) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pull attack-vector seeds out of the protocol's "Test Pass-Through" /
|
|
43
|
+
* "broken patches" section, if present. Falls back to an empty array.
|
|
44
|
+
*
|
|
45
|
+
* The protocol enumerates examples in a `- Remove the listener entirely`
|
|
46
|
+
* style — we extract those bullets verbatim (clipped) so the worker can
|
|
47
|
+
* read them inline without re-fetching the protocol.
|
|
48
|
+
*/
|
|
49
|
+
function _attackVectorSeeds(protocolText) {
|
|
50
|
+
if (!protocolText) return [];
|
|
51
|
+
const out = [];
|
|
52
|
+
// Match the section that explicitly lists broken-patch examples.
|
|
53
|
+
// (heading variants: "broken patch", "Test Pass-Through", "Attack Categories")
|
|
54
|
+
const headings = [
|
|
55
|
+
/^[#]+[^\n]*broken patches?[^\n]*$/im,
|
|
56
|
+
/^[#]+[^\n]*Test Pass-Through[^\n]*$/im,
|
|
57
|
+
/^[#]+\s+Attack Categories[^\n]*$/im,
|
|
58
|
+
];
|
|
59
|
+
for (const h of headings) {
|
|
60
|
+
const hm = protocolText.match(h);
|
|
61
|
+
if (!hm) continue;
|
|
62
|
+
const start = hm.index + hm[0].length;
|
|
63
|
+
const remainder = protocolText.slice(start);
|
|
64
|
+
const next = remainder.match(/^[#]+\s+/m);
|
|
65
|
+
const body = next ? remainder.slice(0, next.index) : remainder;
|
|
66
|
+
for (const line of body.split(/\r?\n/)) {
|
|
67
|
+
const bm = line.match(/^\s+[-*]\s+(.+)$/);
|
|
68
|
+
if (!bm) continue;
|
|
69
|
+
let item = bm[1].replace(/`/g, '').replace(/\*\*/g, '').trim();
|
|
70
|
+
if (item.length > 160) item = item.slice(0, 157) + '...';
|
|
71
|
+
out.push(item);
|
|
72
|
+
if (out.length >= 8) break;
|
|
73
|
+
}
|
|
74
|
+
if (out.length) break;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _scanContracts(projectDir) {
|
|
80
|
+
// Pure scan; caller decides which subset to record as sources.
|
|
81
|
+
const dir = path.join(projectDir, '.gsd-t', 'contracts');
|
|
82
|
+
let entries;
|
|
83
|
+
try { entries = fs.readdirSync(dir); } catch (_) { return []; }
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const f of entries) {
|
|
86
|
+
if (!f.endsWith('.md')) continue;
|
|
87
|
+
const rel = '.gsd-t/contracts/' + f;
|
|
88
|
+
const text = _readMaybe(path.join(projectDir, rel));
|
|
89
|
+
if (!text) continue;
|
|
90
|
+
const m = text.match(/Status:\s*\**\s*(STABLE|DRAFT|PROPOSED)/i);
|
|
91
|
+
out.push({ path: rel, status: m ? m[1].toUpperCase() : 'UNKNOWN' });
|
|
92
|
+
}
|
|
93
|
+
out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function collect(ctx) {
|
|
98
|
+
const { projectDir, recordSource } = ctx;
|
|
99
|
+
|
|
100
|
+
recordSource(PROTOCOL);
|
|
101
|
+
|
|
102
|
+
const protocolText = _readMaybe(path.join(projectDir, PROTOCOL));
|
|
103
|
+
const seeds = _attackVectorSeeds(protocolText);
|
|
104
|
+
const recentCommits = _recentCommits(projectDir);
|
|
105
|
+
const allContracts = _scanContracts(projectDir);
|
|
106
|
+
const enumerated = allContracts.filter((c) => c.status === 'DRAFT' || c.status === 'PROPOSED');
|
|
107
|
+
for (const c of enumerated) recordSource(c.path);
|
|
108
|
+
const counts = { STABLE: 0, DRAFT: 0, PROPOSED: 0, UNKNOWN: 0 };
|
|
109
|
+
for (const c of allContracts) counts[c.status] = (counts[c.status] || 0) + 1;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
scope: { owned: [], notOwned: [], deliverables: [] },
|
|
113
|
+
constraints: [],
|
|
114
|
+
contracts: enumerated,
|
|
115
|
+
ancillary: {
|
|
116
|
+
attackVectorSeeds: seeds,
|
|
117
|
+
contractCount: allContracts.length,
|
|
118
|
+
contractStatusCounts: counts,
|
|
119
|
+
protocolPath: PROTOCOL,
|
|
120
|
+
recentCommits,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
name: NAME,
|
|
127
|
+
requiresSources: [PROTOCOL],
|
|
128
|
+
collect,
|
|
129
|
+
_recentCommits,
|
|
130
|
+
_attackVectorSeeds,
|
|
131
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scan kind collector — surfaces a repo-file-inventory hash, the prior
|
|
5
|
+
* scan output mtime (if any), and the merged exclusion patterns.
|
|
6
|
+
*
|
|
7
|
+
* Fail-open: missing optional source → null/empty field, brief still written.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const NAME = 'scan';
|
|
16
|
+
const PRIOR_SCAN_OUTPUT = '.gsd-t/scan/output.md';
|
|
17
|
+
const SCAN_EXCLUSIONS = '.gsd-t/scan/exclusions.txt';
|
|
18
|
+
const GITIGNORE = '.gitignore';
|
|
19
|
+
|
|
20
|
+
function _readMaybe(file) {
|
|
21
|
+
try { return fs.readFileSync(file, 'utf8'); } catch (_) { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _gitLsFiles(projectDir) {
|
|
25
|
+
try {
|
|
26
|
+
const stdout = execSync('git ls-files', {
|
|
27
|
+
cwd: projectDir,
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
30
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
31
|
+
});
|
|
32
|
+
return String(stdout || '')
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.map((l) => l.trim())
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
} catch (_) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _hashFileList(files) {
|
|
42
|
+
const sorted = files.slice().sort();
|
|
43
|
+
return crypto.createHash('sha256').update(sorted.join('\n')).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Merge `.gitignore` patterns + `.gsd-t/scan/exclusions.txt` (if exists)
|
|
48
|
+
* into a deduplicated, sorted list. Strips comment + blank lines.
|
|
49
|
+
*/
|
|
50
|
+
function _mergedExclusions(projectDir, recordSource) {
|
|
51
|
+
const out = new Set();
|
|
52
|
+
const giText = _readMaybe(path.join(projectDir, GITIGNORE));
|
|
53
|
+
if (giText != null) {
|
|
54
|
+
recordSource(GITIGNORE);
|
|
55
|
+
for (const l of giText.split(/\r?\n/)) {
|
|
56
|
+
const t = l.trim();
|
|
57
|
+
if (!t || t.startsWith('#')) continue;
|
|
58
|
+
out.add(t);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const seText = _readMaybe(path.join(projectDir, SCAN_EXCLUSIONS));
|
|
62
|
+
if (seText != null) {
|
|
63
|
+
recordSource(SCAN_EXCLUSIONS);
|
|
64
|
+
for (const l of seText.split(/\r?\n/)) {
|
|
65
|
+
const t = l.trim();
|
|
66
|
+
if (!t || t.startsWith('#')) continue;
|
|
67
|
+
out.add(t);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const arr = Array.from(out);
|
|
71
|
+
arr.sort();
|
|
72
|
+
return arr;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _priorScanMtime(projectDir, recordSource) {
|
|
76
|
+
const full = path.join(projectDir, PRIOR_SCAN_OUTPUT);
|
|
77
|
+
if (!fs.existsSync(full)) return null;
|
|
78
|
+
recordSource(PRIOR_SCAN_OUTPUT);
|
|
79
|
+
try {
|
|
80
|
+
const stat = fs.statSync(full);
|
|
81
|
+
return new Date(stat.mtimeMs).toISOString();
|
|
82
|
+
} catch (_) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function collect(ctx) {
|
|
88
|
+
const { projectDir, recordSource } = ctx;
|
|
89
|
+
|
|
90
|
+
const files = _gitLsFiles(projectDir);
|
|
91
|
+
const inventoryHash = files.length ? _hashFileList(files) : null;
|
|
92
|
+
const inventoryCount = files.length;
|
|
93
|
+
|
|
94
|
+
const exclusions = _mergedExclusions(projectDir, recordSource);
|
|
95
|
+
const priorScanMtime = _priorScanMtime(projectDir, recordSource);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
scope: { owned: [], notOwned: [], deliverables: [] },
|
|
99
|
+
constraints: [],
|
|
100
|
+
contracts: [],
|
|
101
|
+
ancillary: {
|
|
102
|
+
exclusions,
|
|
103
|
+
inventoryCount,
|
|
104
|
+
inventoryHash,
|
|
105
|
+
priorScanMtime,
|
|
106
|
+
priorScanPath: priorScanMtime ? PRIOR_SCAN_OUTPUT : null,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
name: NAME,
|
|
113
|
+
requiresSources: [],
|
|
114
|
+
collect,
|
|
115
|
+
_gitLsFiles,
|
|
116
|
+
_hashFileList,
|
|
117
|
+
_mergedExclusions,
|
|
118
|
+
};
|