@asobacloud/nehanda 0.0.0-stage → 0.3.0

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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.md +57 -0
  3. package/README.md +444 -2
  4. package/bin/agent.mjs +1260 -0
  5. package/bin/nehanda-ui.mjs +824 -0
  6. package/lib/anthropicHeaders.mjs +8 -0
  7. package/lib/app.mjs +313 -0
  8. package/lib/auth.mjs +255 -0
  9. package/lib/bashguard.mjs +159 -0
  10. package/lib/compact.mjs +306 -0
  11. package/lib/energy-middleware.mjs +318 -0
  12. package/lib/hookShapeOrder.mjs +30 -0
  13. package/lib/hookplane.mjs +228 -0
  14. package/lib/instructions.mjs +23 -0
  15. package/lib/jev_read.mjs +216 -0
  16. package/lib/jev_write.mjs +253 -0
  17. package/lib/laya.mjs +296 -0
  18. package/lib/laya_sidecar.mjs +198 -0
  19. package/lib/mcp-config.mjs +189 -0
  20. package/lib/mcp.mjs +169 -0
  21. package/lib/modelConfig.mjs +152 -0
  22. package/lib/modelDiscovery.mjs +205 -0
  23. package/lib/openaiCompat.mjs +147 -0
  24. package/lib/orchestrate.mjs +1163 -0
  25. package/lib/paths.mjs +20 -0
  26. package/lib/permissions.mjs +44 -0
  27. package/lib/scripts/__pycache__/laya_worker.cpython-314.pyc +0 -0
  28. package/lib/scripts/audit-code-integrity.py +437 -0
  29. package/lib/scripts/ensure-laya-env.sh +55 -0
  30. package/lib/scripts/js-safety-checker.cjs +308 -0
  31. package/lib/scripts/laya_worker.py +171 -0
  32. package/lib/scripts/odse-transform.py +97 -0
  33. package/lib/scripts/python-safety-checker.py +197 -0
  34. package/lib/scripts/shell-safety-checker.sh +306 -0
  35. package/lib/settings.mjs +147 -0
  36. package/lib/store.mjs +278 -0
  37. package/lib/team.mjs +69 -0
  38. package/lib/testgen.mjs +146 -0
  39. package/lib/tools/AuditCodeIntegrity.json +30 -0
  40. package/lib/tools/JsSafetyChecker.json +29 -0
  41. package/lib/tools/PythonSafetyChecker.json +29 -0
  42. package/lib/tools/ShellSafetyChecker.json +29 -0
  43. package/lib/tools.mjs +907 -0
  44. package/lib/transcript.mjs +223 -0
  45. package/lib/ui.mjs +286 -0
  46. package/lib/workflow.mjs +91 -0
  47. package/lib/worktree.mjs +56 -0
  48. package/package.json +50 -4
  49. package/schema.sql +181 -0
  50. package/scripts/postinstall.mjs +62 -0
  51. package/scripts/sdlc-acceptance.sh +477 -0
  52. package/scripts/verify-sdlc-hook-order.mjs +26 -0
  53. package/templates/test_e2e.sh +32 -0
  54. package/templates/test_file.sh +14 -0
  55. package/templates/test_hook.sh +27 -0
  56. package/templates/test_http.sh +17 -0
  57. package/templates/test_phase.sh +23 -0
  58. package/templates/test_process.sh +14 -0
  59. package/templates/test_tool.sh +24 -0
  60. package/templates/validate_test.sh +55 -0
package/lib/store.mjs ADDED
@@ -0,0 +1,278 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import Database from 'better-sqlite3'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const SCHEMA_PATH = path.resolve(__dirname, '..', 'schema.sql')
8
+
9
+ /** Current schema version after Jev-Mem tables/columns. */
10
+ export const SCHEMA_VERSION = 2
11
+
12
+ const byPath = new Map()
13
+
14
+ const MEMORY_JEV_COLUMNS = [
15
+ ['session_id', 'TEXT'],
16
+ ['conversation_id', 'TEXT'],
17
+ ['provenance', 'TEXT'],
18
+ ['entities_json', 'TEXT'],
19
+ ['t_episodic', 'REAL DEFAULT 0'],
20
+ ['t_semantic', 'REAL DEFAULT 0'],
21
+ ['t_procedural', 'REAL DEFAULT 0'],
22
+ ['t_preference', 'REAL DEFAULT 0'],
23
+ ['source_entry_id', 'INTEGER'],
24
+ ['observation_ts', 'INTEGER'],
25
+ ]
26
+
27
+ function tableExists(db, name) {
28
+ const row = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`).get(name)
29
+ return Boolean(row)
30
+ }
31
+
32
+ function columnNames(db, table) {
33
+ return db.prepare(`PRAGMA table_info(${table})`).all().map(r => r.name)
34
+ }
35
+
36
+ /** Apply incremental migrations for existing DBs that already have schema_version < SCHEMA_VERSION. */
37
+ export function migrateStore(db) {
38
+ const row = db.prepare(`SELECT value FROM schema_meta WHERE key = 'schema_version'`).get()
39
+ let version = row ? Number(row.value) : 0
40
+ if (!Number.isFinite(version)) version = 0
41
+
42
+ if (version < 2) {
43
+ // Extend memories with Jev-Mem node fields (CREATE IF NOT EXISTS already ran from schema.sql
44
+ // for fresh DBs; ALTER covers DBs created under v1).
45
+ if (tableExists(db, 'memories')) {
46
+ const cols = new Set(columnNames(db, 'memories'))
47
+ for (const [name, decl] of MEMORY_JEV_COLUMNS) {
48
+ if (!cols.has(name)) {
49
+ db.exec(`ALTER TABLE memories ADD COLUMN ${name} ${decl}`)
50
+ }
51
+ }
52
+ }
53
+
54
+ db.exec(`
55
+ CREATE TABLE IF NOT EXISTS memory_edges (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ src_id TEXT NOT NULL,
58
+ dst_id TEXT NOT NULL,
59
+ relation TEXT NOT NULL CHECK (relation IN ('semantic', 'temporal', 'causal', 'entity')),
60
+ weight REAL NOT NULL DEFAULT 0,
61
+ meta_json TEXT,
62
+ created_at INTEGER NOT NULL,
63
+ UNIQUE(src_id, dst_id, relation)
64
+ );
65
+ CREATE INDEX IF NOT EXISTS idx_memory_edges_src ON memory_edges(src_id, relation);
66
+ CREATE INDEX IF NOT EXISTS idx_memory_edges_dst ON memory_edges(dst_id, relation);
67
+ CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id, observation_ts);
68
+
69
+ CREATE TABLE IF NOT EXISTS laya_jev_state (
70
+ session_id TEXT PRIMARY KEY,
71
+ task_status TEXT NOT NULL DEFAULT 'investigating'
72
+ CHECK (task_status IN (
73
+ 'investigating', 'modifying_code', 'awaiting_user',
74
+ 'verifying', 'blocked', 'idle'
75
+ )),
76
+ file_target TEXT,
77
+ escalation_risk INTEGER NOT NULL DEFAULT 0 CHECK (escalation_risk IN (0, 1)),
78
+ confidence_score REAL NOT NULL DEFAULT 0,
79
+ state_json TEXT,
80
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
81
+ );
82
+ `)
83
+
84
+ db.prepare(
85
+ `INSERT INTO schema_meta(key,value) VALUES ('schema_version', ?)
86
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
87
+ ).run(String(SCHEMA_VERSION))
88
+ version = SCHEMA_VERSION
89
+ }
90
+
91
+ return version
92
+ }
93
+
94
+ /** §4.8 — single writer connection per DB path; pragmas on open. */
95
+ export function openStore(dbPath) {
96
+ const abs = path.resolve(dbPath)
97
+ if (byPath.has(abs)) return byPath.get(abs)
98
+ const dir = path.dirname(abs)
99
+ fs.mkdirSync(dir, { recursive: true })
100
+ const db = new Database(abs)
101
+ db.pragma('foreign_keys = ON')
102
+ db.pragma('journal_mode = WAL')
103
+ db.pragma('busy_timeout = 30000')
104
+ const ddl = fs.readFileSync(SCHEMA_PATH, 'utf8')
105
+ db.exec(ddl)
106
+ const row = db.prepare(`SELECT value FROM schema_meta WHERE key = 'schema_version'`).get()
107
+ if (!row) {
108
+ db.prepare(`INSERT INTO schema_meta(key,value) VALUES ('schema_version','1')`).run()
109
+ }
110
+ migrateStore(db)
111
+ byPath.set(abs, db)
112
+ return db
113
+ }
114
+
115
+ /** Drop a cached connection (tests). */
116
+ export function closeStore(dbPath) {
117
+ const abs = path.resolve(dbPath)
118
+ const db = byPath.get(abs)
119
+ if (db) {
120
+ db.close()
121
+ byPath.delete(abs)
122
+ }
123
+ }
124
+
125
+ /** Run fn inside a single immediate transaction (writer serialization). */
126
+ export function withTransaction(db, fn) {
127
+ return db.transaction(fn)()
128
+ }
129
+
130
+ const TASK_STATUSES = new Set([
131
+ 'investigating', 'modifying_code', 'awaiting_user', 'verifying', 'blocked', 'idle',
132
+ ])
133
+
134
+ /** Idempotent upsert of System-1 canonical session state. */
135
+ export function upsertLayaJevState(db, sessionId, state) {
136
+ const taskStatus = TASK_STATUSES.has(state.task_status) ? state.task_status : 'investigating'
137
+ const escalation = state.escalation_risk ? 1 : 0
138
+ const confidence = Number.isFinite(state.confidence_score) ? state.confidence_score : 0
139
+ const stateJson = state.state_json != null
140
+ ? (typeof state.state_json === 'string' ? state.state_json : JSON.stringify(state.state_json))
141
+ : JSON.stringify({
142
+ task_status: taskStatus,
143
+ file_target: state.file_target ?? null,
144
+ escalation_risk: Boolean(escalation),
145
+ confidence_score: confidence,
146
+ })
147
+
148
+ db.prepare(`
149
+ INSERT INTO laya_jev_state(session_id, task_status, file_target, escalation_risk, confidence_score, state_json, updated_at)
150
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
151
+ ON CONFLICT(session_id) DO UPDATE SET
152
+ task_status = excluded.task_status,
153
+ file_target = excluded.file_target,
154
+ escalation_risk = excluded.escalation_risk,
155
+ confidence_score = excluded.confidence_score,
156
+ state_json = excluded.state_json,
157
+ updated_at = datetime('now')
158
+ `).run(
159
+ sessionId,
160
+ taskStatus,
161
+ state.file_target ?? null,
162
+ escalation,
163
+ confidence,
164
+ stateJson,
165
+ )
166
+
167
+ return getLayaJevState(db, sessionId)
168
+ }
169
+
170
+ export function getLayaJevState(db, sessionId) {
171
+ return db.prepare(`SELECT * FROM laya_jev_state WHERE session_id = ?`).get(sessionId) || null
172
+ }
173
+
174
+ /** Insert or replace a canonical memory node; always preserves content (no discard-on-write). */
175
+ export function upsertMemoryNode(db, node) {
176
+ const now = node.updated_at ?? Date.now()
177
+ const created = node.created_at ?? now
178
+ db.prepare(`
179
+ INSERT INTO memories(
180
+ id, type, title, content, keywords, anticipated_queries, concept_tags, project_scope,
181
+ correction_count, created_at, updated_at, last_accessed, access_count, attention_score,
182
+ session_id, conversation_id, provenance, entities_json,
183
+ t_episodic, t_semantic, t_procedural, t_preference, source_entry_id, observation_ts
184
+ ) VALUES (
185
+ @id, @type, @title, @content, @keywords, @anticipated_queries, @concept_tags, @project_scope,
186
+ @correction_count, @created_at, @updated_at, @last_accessed, @access_count, @attention_score,
187
+ @session_id, @conversation_id, @provenance, @entities_json,
188
+ @t_episodic, @t_semantic, @t_procedural, @t_preference, @source_entry_id, @observation_ts
189
+ )
190
+ ON CONFLICT(id) DO UPDATE SET
191
+ title = excluded.title,
192
+ content = excluded.content,
193
+ keywords = excluded.keywords,
194
+ concept_tags = excluded.concept_tags,
195
+ updated_at = excluded.updated_at,
196
+ entities_json = excluded.entities_json,
197
+ t_episodic = excluded.t_episodic,
198
+ t_semantic = excluded.t_semantic,
199
+ t_procedural = excluded.t_procedural,
200
+ t_preference = excluded.t_preference,
201
+ attention_score = excluded.attention_score
202
+ `).run({
203
+ id: node.id,
204
+ type: node.type || 'jev_observation',
205
+ title: node.title || '',
206
+ content: node.content ?? '',
207
+ keywords: node.keywords ?? null,
208
+ anticipated_queries: node.anticipated_queries ?? null,
209
+ concept_tags: node.concept_tags ?? null,
210
+ project_scope: node.project_scope ?? null,
211
+ correction_count: node.correction_count ?? 1,
212
+ created_at: created,
213
+ updated_at: now,
214
+ last_accessed: node.last_accessed ?? null,
215
+ access_count: node.access_count ?? 0,
216
+ attention_score: node.attention_score ?? 0.5,
217
+ session_id: node.session_id ?? null,
218
+ conversation_id: node.conversation_id ?? null,
219
+ provenance: node.provenance ?? null,
220
+ entities_json: node.entities_json
221
+ ? (typeof node.entities_json === 'string' ? node.entities_json : JSON.stringify(node.entities_json))
222
+ : null,
223
+ t_episodic: node.t_episodic ?? 0,
224
+ t_semantic: node.t_semantic ?? 0,
225
+ t_procedural: node.t_procedural ?? 0,
226
+ t_preference: node.t_preference ?? 0,
227
+ source_entry_id: node.source_entry_id ?? null,
228
+ observation_ts: node.observation_ts ?? created,
229
+ })
230
+
231
+ // Keep FTS in sync (delete+insert; FTS5 content tables are separate).
232
+ try {
233
+ db.prepare(`DELETE FROM memories_fts WHERE rowid = (SELECT rowid FROM memories WHERE id = ?)`).run(node.id)
234
+ } catch { /* fresh or missing fts row */ }
235
+ try {
236
+ db.prepare(`
237
+ INSERT INTO memories_fts(rowid, title, content, keywords, anticipated_queries)
238
+ SELECT rowid, title, content, keywords, anticipated_queries FROM memories WHERE id = ?
239
+ `).run(node.id)
240
+ } catch { /* FTS optional for unit tests without virtual table quirks */ }
241
+
242
+ return db.prepare(`SELECT * FROM memories WHERE id = ?`).get(node.id)
243
+ }
244
+
245
+ /** Insert/update a typed relation edge when weight ≥ threshold (caller decides). */
246
+ export function upsertMemoryEdge(db, { src_id, dst_id, relation, weight, meta_json = null }) {
247
+ const allowed = new Set(['semantic', 'temporal', 'causal', 'entity'])
248
+ if (!allowed.has(relation)) throw new Error(`invalid relation: ${relation}`)
249
+ const now = Date.now()
250
+ const meta = meta_json == null
251
+ ? null
252
+ : (typeof meta_json === 'string' ? meta_json : JSON.stringify(meta_json))
253
+ db.prepare(`
254
+ INSERT INTO memory_edges(src_id, dst_id, relation, weight, meta_json, created_at)
255
+ VALUES (?, ?, ?, ?, ?, ?)
256
+ ON CONFLICT(src_id, dst_id, relation) DO UPDATE SET
257
+ weight = excluded.weight,
258
+ meta_json = excluded.meta_json
259
+ `).run(src_id, dst_id, relation, weight, meta, now)
260
+ return db.prepare(
261
+ `SELECT * FROM memory_edges WHERE src_id = ? AND dst_id = ? AND relation = ?`,
262
+ ).get(src_id, dst_id, relation)
263
+ }
264
+
265
+ export function listMemoryEdges(db, nodeId, relation = null) {
266
+ if (relation) {
267
+ return db.prepare(`
268
+ SELECT * FROM memory_edges
269
+ WHERE (src_id = ? OR dst_id = ?) AND relation = ?
270
+ ORDER BY weight DESC
271
+ `).all(nodeId, nodeId, relation)
272
+ }
273
+ return db.prepare(`
274
+ SELECT * FROM memory_edges
275
+ WHERE src_id = ? OR dst_id = ?
276
+ ORDER BY weight DESC
277
+ `).all(nodeId, nodeId)
278
+ }
package/lib/team.mjs ADDED
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { onaHome } from './paths.mjs'
4
+
5
+ const TEAMS_DIR = path.join(onaHome(), 'teams')
6
+
7
+ function teamFilePath(name) {
8
+ return path.join(TEAMS_DIR, `${name.replace(/[^a-zA-Z0-9._-]/g, '_')}.json`)
9
+ }
10
+
11
+ export function createTeam(name, leadSessionId) {
12
+ fs.mkdirSync(TEAMS_DIR, { recursive: true })
13
+ const team = {
14
+ name,
15
+ createdAt: Date.now(),
16
+ leadSessionId,
17
+ members: [],
18
+ }
19
+ fs.writeFileSync(teamFilePath(name), JSON.stringify(team, null, 2), 'utf8')
20
+ return team
21
+ }
22
+
23
+ export function getTeam(name) {
24
+ const p = teamFilePath(name)
25
+ if (!fs.existsSync(p)) return null
26
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')) } catch { return null }
27
+ }
28
+
29
+ export function listTeams() {
30
+ if (!fs.existsSync(TEAMS_DIR)) return []
31
+ return fs.readdirSync(TEAMS_DIR)
32
+ .filter(f => f.endsWith('.json'))
33
+ .map(f => {
34
+ try { return JSON.parse(fs.readFileSync(path.join(TEAMS_DIR, f), 'utf8')) }
35
+ catch { return null }
36
+ })
37
+ .filter(Boolean)
38
+ }
39
+
40
+ export function addTeammate(teamName, memberName, sessionId) {
41
+ const team = getTeam(teamName)
42
+ if (!team) return null
43
+ const agentId = `${memberName}@${teamName}`
44
+ const member = {
45
+ agentId,
46
+ name: memberName,
47
+ isActive: true,
48
+ sessionId,
49
+ joinedAt: Date.now(),
50
+ }
51
+ team.members.push(member)
52
+ fs.writeFileSync(teamFilePath(teamName), JSON.stringify(team, null, 2), 'utf8')
53
+ return member
54
+ }
55
+
56
+ export function setTeammateIdle(teamName, agentId) {
57
+ const team = getTeam(teamName)
58
+ if (!team) return false
59
+ const member = team.members.find(m => m.agentId === agentId)
60
+ if (!member) return false
61
+ member.isActive = false
62
+ fs.writeFileSync(teamFilePath(teamName), JSON.stringify(team, null, 2), 'utf8')
63
+ return true
64
+ }
65
+
66
+ export function deleteTeam(name) {
67
+ const p = teamFilePath(name)
68
+ try { fs.unlinkSync(p); return true } catch { return false }
69
+ }
@@ -0,0 +1,146 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { spawnSync } from 'node:child_process'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { withTransaction } from './store.mjs'
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+ const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates')
9
+ const TAG_RE = /\[template:\s*(\w+)\]/
10
+ const GENERATED_DIR = path.resolve(__dirname, '..', '.generated-tests')
11
+
12
+ /** Extract success criteria with their template tags from plan content. */
13
+ export function extractCriteriaWithTags(planContent) {
14
+ const lines = planContent.split('\n')
15
+ let inCriteria = false
16
+ const criteria = []
17
+ for (const line of lines) {
18
+ if (/^##\s*Success\s*Criteria/i.test(line)) { inCriteria = true; continue }
19
+ if (inCriteria && /^##\s/.test(line)) break
20
+ if (inCriteria && line.trim().match(/^\d+\.|^-/)) {
21
+ const m = line.match(TAG_RE)
22
+ if (m) {
23
+ const text = line.trim().replace(TAG_RE, '').replace(/^\d+\.\s*|^-\s*/, '').trim()
24
+ criteria.push({ text, template: m[1] })
25
+ }
26
+ }
27
+ }
28
+ return criteria
29
+ }
30
+
31
+ /**
32
+ * Generate and run tests from plan criteria using templates.
33
+ * For now, generates simple structural tests from the template + criterion.
34
+ * The LLM-based slot filling is a future enhancement — this version creates
35
+ * runnable tests by pattern-matching on the criterion text.
36
+ */
37
+ export async function generateAndRunTests(db, rt, planContent, log) {
38
+ const criteria = extractCriteriaWithTags(planContent)
39
+ if (!criteria.length) { log('No tagged criteria found in plan.'); return [] }
40
+
41
+ fs.mkdirSync(GENERATED_DIR, { recursive: true })
42
+ const results = []
43
+
44
+ for (let i = 0; i < criteria.length; i++) {
45
+ const { text, template } = criteria[i]
46
+ const templatePath = path.join(TEMPLATES_DIR, `test_${template}.sh`)
47
+ if (!fs.existsSync(templatePath)) {
48
+ log(` ✗ No template file for: ${template}`)
49
+ results.push({ criterion: text, passed: false, output: `Missing template: ${template}` })
50
+ continue
51
+ }
52
+
53
+ const templateContent = fs.readFileSync(templatePath, 'utf8')
54
+ const testFile = path.join(GENERATED_DIR, `test_${i}_${template}.sh`)
55
+
56
+ // Fill template slots with criterion-derived content
57
+ const filled = fillTemplate(templateContent, text, template, rt)
58
+ fs.writeFileSync(testFile, filled, { mode: 0o755 })
59
+
60
+ // Validate
61
+ const validate = spawnSync('bash', [path.join(TEMPLATES_DIR, 'validate_test.sh'), testFile], { encoding: 'utf8', timeout: 10_000 })
62
+ if (validate.status !== 0) {
63
+ log(` ✗ Validation failed: ${text.slice(0, 60)}`)
64
+ results.push({ criterion: text, passed: false, output: validate.stderr || validate.stdout })
65
+ continue
66
+ }
67
+
68
+ // Run
69
+ const dbPath = rt.runtimeDbPath || process.env.AGENT_SDLC_DB
70
+ const run = spawnSync('bash', [testFile], {
71
+ encoding: 'utf8',
72
+ timeout: 60_000,
73
+ env: { ...process.env, AGENT_SDLC_DB: dbPath },
74
+ })
75
+ const passed = run.status === 0
76
+ const output = ((run.stdout || '') + (run.stderr || '')).trim()
77
+ results.push({ criterion: text, passed, output })
78
+
79
+ // Persist result
80
+ withTransaction(db, () => {
81
+ db.prepare(`INSERT INTO events(conversation_id, session_id, event_type, detail) VALUES (?,?,?,?)`).run(
82
+ rt.conversationId, rt.sessionId, 'test_result',
83
+ JSON.stringify({ criterion: text, template, passed, output: output.slice(0, 2000) })
84
+ )
85
+ })
86
+ }
87
+
88
+ return results
89
+ }
90
+
91
+ /**
92
+ * Fill template slots based on criterion text and template type.
93
+ * This is a structural fill — it creates a runnable test that exercises
94
+ * the product through its CLI. For full LLM-based slot filling, the
95
+ * /test command would call the model with the template + criterion.
96
+ */
97
+ function fillTemplate(templateContent, criterion, templateType, rt) {
98
+ const dbPath = rt.runtimeDbPath || process.env.AGENT_SDLC_DB || '/tmp/sdlc_test_$$.db'
99
+ const onaPath = path.resolve(__dirname, '..', 'bin', 'agent.mjs')
100
+ const onaCmd = `node ${onaPath}`
101
+
102
+ let filled = templateContent
103
+ .replace(/^# PLAN_REQ:.*$/m, `# PLAN_REQ: ${criterion}`)
104
+
105
+ // Remove comment markers from the example code in templates so they're executable
106
+ // Replace placeholder sections with actual test code based on template type
107
+ if (templateType === 'tool_contract') {
108
+ filled = fillToolContractTemplate(filled, criterion, onaCmd, dbPath)
109
+ } else if (templateType === 'phase_transition') {
110
+ filled = fillPhaseTransitionTemplate(filled, criterion, onaCmd, dbPath)
111
+ } else if (templateType === 'hook_contract') {
112
+ filled = fillHookContractTemplate(filled, criterion, onaCmd, dbPath)
113
+ } else if (templateType === 'e2e_workflow') {
114
+ filled = fillE2ETemplate(filled, criterion, onaCmd, dbPath)
115
+ }
116
+
117
+ return filled
118
+ }
119
+
120
+ function fillToolContractTemplate(template, criterion, onaCmd, dbPath) {
121
+ return template
122
+ .replace(/# ══ SETUP ══\n[\s\S]*?(?=# ══ EXERCISE ══)/, `# ══ SETUP ══\necho "test content" > /tmp/sdlc_test_file.txt\n\n`)
123
+ .replace(/# ══ EXERCISE ══\n[\s\S]*?(?=# ══ ASSERT ══)/, `# ══ EXERCISE ══\n${onaCmd} --eval '{"tool":"Read","input":{"file_path":"/tmp/sdlc_test_file.txt"}}'\n\n`)
124
+ .replace(/# ══ ASSERT ══[\s\S]*$/, `# ══ ASSERT ══\nRESULT=$(sqlite3 "$AGENT_SDLC_DB" "SELECT payload_json FROM transcript_entries WHERE entry_type='tool_result' ORDER BY sequence DESC LIMIT 1")\necho "$RESULT" | grep '"is_error":false' || { echo "FAIL: ${criterion}"; exit 1; }\necho "PASS: ${criterion}"\n`)
125
+ }
126
+
127
+ function fillPhaseTransitionTemplate(template, criterion, onaCmd, dbPath) {
128
+ return template
129
+ .replace(/# ══ SETUP ══\n[\s\S]*?(?=# ══ EXERCISE ══)/, `# ══ SETUP ══\n${onaCmd} --init-db\nsqlite3 "$AGENT_SDLC_DB" "INSERT INTO conversations(id, project_dir, phase) VALUES ('test-conv', '/tmp', 'idle')"\n\n`)
130
+ .replace(/# ══ EXERCISE ══\n[\s\S]*?(?=# ══ ASSERT ══)/, `# ══ EXERCISE ══\n${onaCmd} --transition planning --conversation test-conv 2>&1 || true\n\n`)
131
+ .replace(/# ══ ASSERT ══[\s\S]*$/, `# ══ ASSERT ══\nPHASE=$(sqlite3 "$AGENT_SDLC_DB" "SELECT phase FROM conversations WHERE id='test-conv'")\ntest "$PHASE" = "planning" || { echo "FAIL: ${criterion}"; exit 1; }\necho "PASS: ${criterion}"\n`)
132
+ }
133
+
134
+ function fillHookContractTemplate(template, criterion, onaCmd, dbPath) {
135
+ return template
136
+ .replace(/# ══ SETUP ══\n[\s\S]*?(?=# ══ EXERCISE ══)/, `# ══ SETUP ══\n${onaCmd} --init-db\nsqlite3 "$AGENT_SDLC_DB" "INSERT OR REPLACE INTO settings_snapshot(scope, json, updated_at) VALUES ('effective', '{\\\"hooks\\\":[{\\\"hook_event_name\\\":\\\"PreToolUse\\\",\\\"matcher\\\":\\\"Bash\\\",\\\"command\\\":\\\"exit 2\\\"}]}', datetime('now'))"\n\n`)
137
+ .replace(/# ══ EXERCISE ══\n[\s\S]*?(?=# ══ ASSERT ══)/, `# ══ EXERCISE ══\n${onaCmd} --eval '{"tool":"Bash","input":{"command":"echo hi"}}' || true\n\n`)
138
+ .replace(/# ══ ASSERT ══[\s\S]*$/, `# ══ ASSERT ══\nROW=$(sqlite3 "$AGENT_SDLC_DB" "SELECT exit_code FROM hook_invocations WHERE hook_event='PreToolUse' ORDER BY id DESC LIMIT 1")\ntest "$ROW" = "2" || { echo "FAIL: ${criterion}"; exit 1; }\necho "PASS: ${criterion}"\n`)
139
+ }
140
+
141
+ function fillE2ETemplate(template, criterion, onaCmd, dbPath) {
142
+ return template
143
+ .replace(/# ══ SETUP ══\n[\s\S]*?(?=# ══ EXERCISE ══)/, `# ══ SETUP ══\n${onaCmd} --init-db\n\n`)
144
+ .replace(/# ══ EXERCISE ══\n[\s\S]*?(?=# ══ ASSERT ══)/, `# ══ EXERCISE ══\n# E2E workflow test — verify DB was initialized\n${onaCmd} --init-db\n\n`)
145
+ .replace(/# ══ ASSERT ══[\s\S]*$/, `# ══ ASSERT ══\nTABLES=$(sqlite3 "$AGENT_SDLC_DB" "SELECT count(*) FROM sqlite_master WHERE type='table'")\ntest "$TABLES" -ge 13 || { echo "FAIL: ${criterion}"; exit 1; }\necho "PASS: ${criterion}"\n`)
146
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "AuditCodeIntegrity",
3
+ "description": "Run the code integrity auditor (lifecycle teardown parity, mock-theater test detection, naming invariants, swallowed exceptions) against a file or directory. Returns structured JSON findings.",
4
+ "input_schema": {
5
+ "type": "object",
6
+ "properties": {
7
+ "target": { "type": "string", "description": "File or directory to audit (default: cwd)" },
8
+ "timeout": { "type": "integer", "description": "Timeout in ms (max 600000)" }
9
+ }
10
+ },
11
+ "phases": {
12
+ "explore_only": true,
13
+ "planning_blocked": false,
14
+ "mandatory_in": ["test"]
15
+ },
16
+ "prompt": {
17
+ "mandatory_instruction": "Run AuditCodeIntegrity on the workspace to check for lifecycle teardown parity, mock-theater tests, naming invariants, and swallowed exceptions.",
18
+ "available_hint": "Audits code integrity (lifecycle parity, mock-theater tests, naming invariants, error handling)"
19
+ },
20
+ "execution": {
21
+ "runtime": "python3",
22
+ "script": "lib/scripts/audit-code-integrity.py",
23
+ "args": [
24
+ "--target", "{{target}}",
25
+ "--format", "json"
26
+ ],
27
+ "default_timeout": 120000,
28
+ "max_timeout": 600000
29
+ }
30
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "JsSafetyChecker",
3
+ "description": "Run the JavaScript safety checker (AST-based duplicate function, duplicate HTML ID, syntax errors) against a file or directory. Returns findings.",
4
+ "input_schema": {
5
+ "type": "object",
6
+ "properties": {
7
+ "target": { "type": "string", "description": "Target directory or file to scan (default: cwd)" },
8
+ "timeout": { "type": "integer", "description": "Timeout in ms (max 600000)" }
9
+ }
10
+ },
11
+ "phases": {
12
+ "explore_only": true,
13
+ "planning_blocked": false,
14
+ "mandatory_in": ["test"]
15
+ },
16
+ "prompt": {
17
+ "mandatory_instruction": "Run JsSafetyChecker on JavaScript and HTML files in the workspace to validate syntax, duplicate functions, and duplicate IDs.",
18
+ "available_hint": "Validates JS/HTML for syntax errors, duplicate functions, and duplicate element IDs"
19
+ },
20
+ "execution": {
21
+ "runtime": "node",
22
+ "script": "lib/scripts/js-safety-checker.cjs",
23
+ "args": [
24
+ "{{target}}"
25
+ ],
26
+ "default_timeout": 120000,
27
+ "max_timeout": 600000
28
+ }
29
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "PythonSafetyChecker",
3
+ "description": "Run the Python safety checker (bandit security issues, ruff style/lint issues, AST mutable default arg checks) against Python files. Returns findings.",
4
+ "input_schema": {
5
+ "type": "object",
6
+ "properties": {
7
+ "target": { "type": "string", "description": "Target directory or file to scan (default: cwd)" },
8
+ "timeout": { "type": "integer", "description": "Timeout in ms (max 600000)" }
9
+ }
10
+ },
11
+ "phases": {
12
+ "explore_only": true,
13
+ "planning_blocked": false,
14
+ "mandatory_in": ["test"]
15
+ },
16
+ "prompt": {
17
+ "mandatory_instruction": "Run PythonSafetyChecker on Python files in the workspace to validate for security issues, lint errors, and unsafe patterns.",
18
+ "available_hint": "Validates Python files for security (bandit), lint (ruff), and unsafe patterns (eval, hardcoded credentials)"
19
+ },
20
+ "execution": {
21
+ "runtime": "python3",
22
+ "script": "lib/scripts/python-safety-checker.py",
23
+ "args": [
24
+ "{{target}}"
25
+ ],
26
+ "default_timeout": 120000,
27
+ "max_timeout": 600000
28
+ }
29
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "ShellSafetyChecker",
3
+ "description": "Run the shell safety checker (missing set -euo pipefail, background job silent failure risk, hardcoded credentials) against shell scripts. Returns findings.",
4
+ "input_schema": {
5
+ "type": "object",
6
+ "properties": {
7
+ "target": { "type": "string", "description": "Target directory or file to scan (default: cwd)" },
8
+ "timeout": { "type": "integer", "description": "Timeout in ms (max 600000)" }
9
+ }
10
+ },
11
+ "phases": {
12
+ "explore_only": true,
13
+ "planning_blocked": false,
14
+ "mandatory_in": ["test"]
15
+ },
16
+ "prompt": {
17
+ "mandatory_instruction": "Run ShellSafetyChecker on shell scripts in the workspace to validate strict mode, silent failure risks, and security issues.",
18
+ "available_hint": "Validates shell scripts for safety (strict mode, credentials, background job risks)"
19
+ },
20
+ "execution": {
21
+ "runtime": "bash",
22
+ "script": "lib/scripts/shell-safety-checker.sh",
23
+ "args": [
24
+ "{{target}}"
25
+ ],
26
+ "default_timeout": 120000,
27
+ "max_timeout": 600000
28
+ }
29
+ }