@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
@@ -0,0 +1,477 @@
1
+ #!/usr/bin/env bash
2
+ # SDLC acceptance — black-box checks per CLEAN_ROOM_SPEC.md
3
+ # Invokes: node "$ONA", sqlite3 "$AGENT_SDLC_DB", python3 (mock HTTP only). No Node imports.
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
7
+ REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
8
+ export ONA=${ONA:-"$REPO_ROOT/bin/agent.mjs"}
9
+
10
+ PASS=0
11
+ SKIP=0
12
+ TOTAL=0
13
+
14
+ cleanup() {
15
+ local ec=$?
16
+ if [[ -n "${MOCK_PID:-}" ]]; then kill "$MOCK_PID" 2>/dev/null || true; wait "$MOCK_PID" 2>/dev/null || true; fi
17
+ [[ -n "${ACCEPT_TMP:-}" && -d "$ACCEPT_TMP" ]] && rm -rf "$ACCEPT_TMP" || true
18
+ return "$ec"
19
+ }
20
+ trap cleanup EXIT
21
+
22
+ ACCEPT_TMP=$(mktemp -d "${TMPDIR:-/tmp}/sdlc-accept.XXXXXX")
23
+ export ACCEPT_TMP
24
+
25
+ command -v node >/dev/null 2>&1 || { echo "sdlc-acceptance: node required" >&2; exit 2; }
26
+ command -v sqlite3 >/dev/null 2>&1 || { echo "sdlc-acceptance: sqlite3 required" >&2; exit 2; }
27
+ command -v python3 >/dev/null 2>&1 || { echo "sdlc-acceptance: python3 required" >&2; exit 2; }
28
+ [[ -f "$ONA" ]] || { echo "sdlc-acceptance: ONA not found: $ONA" >&2; exit 2; }
29
+
30
+ run_check() {
31
+ local row_id="$1"; shift
32
+ TOTAL=$((TOTAL + 1))
33
+ if "$@"; then
34
+ PASS=$((PASS + 1)); echo " PASS: $row_id"
35
+ else
36
+ echo " FAIL: $row_id" >&2; exit 1
37
+ fi
38
+ }
39
+
40
+ skip_row() {
41
+ local row_id="$1" reason="$2"
42
+ TOTAL=$((TOTAL + 1)); SKIP=$((SKIP + 1))
43
+ echo " SKIP: $row_id — $reason" >&2
44
+ }
45
+
46
+ ona_pipe() {
47
+ local input="$1"
48
+ { printf '%s\n' "$input"; } | node "$ONA" 2>&1
49
+ }
50
+
51
+ db() {
52
+ sqlite3 -cmd ".output /dev/null" -cmd "PRAGMA foreign_keys=ON;" -cmd "PRAGMA busy_timeout=30000;" -cmd ".output stdout" "$AGENT_SDLC_DB" "$@"
53
+ }
54
+
55
+ put_effective_json() {
56
+ local json="$1"
57
+ local esc="${json//\'/\'\'}"
58
+ db "INSERT OR REPLACE INTO settings_snapshot(scope,json,updated_at) VALUES ('effective','$esc',datetime('now'))"
59
+ }
60
+
61
+ fresh_db() {
62
+ export AGENT_SDLC_DB="$ACCEPT_TMP/db_${1}.db"
63
+ rm -f "$AGENT_SDLC_DB" "$AGENT_SDLC_DB-wal" "$AGENT_SDLC_DB-shm" 2>/dev/null || true
64
+ SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" --init-db >/dev/null 2>&1
65
+ }
66
+
67
+ stop_mock() {
68
+ if [[ -n "${MOCK_PID:-}" ]]; then
69
+ kill "$MOCK_PID" 2>/dev/null || true
70
+ wait "$MOCK_PID" 2>/dev/null || true
71
+ MOCK_PID=
72
+ fi
73
+ }
74
+
75
+ MOCK_PORT=18765
76
+
77
+ write_mock_server() {
78
+ local port="$1"
79
+ export MOCK_PORT="$port"
80
+ local py="$ACCEPT_TMP/mock_openai.py"
81
+ cat >"$py" <<PY
82
+ import json, re
83
+ from http.server import HTTPServer, BaseHTTPRequestHandler
84
+
85
+ def tool_resp(name, args_obj):
86
+ return {"id": "call_sdlc", "type": "function", "function": {"name": name, "arguments": json.dumps(args_obj)}}
87
+
88
+ class H(BaseHTTPRequestHandler):
89
+ def log_message(self, *a): pass
90
+ def do_GET(self):
91
+ if self.path in ("/v1/models", "/"):
92
+ b = json.dumps({"data": [{"id":"mock-model","object":"model","owned_by":"test"}]}).encode()
93
+ self.send_response(200); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(b))); self.end_headers(); self.wfile.write(b); return
94
+ self.send_error(404)
95
+ def do_POST(self):
96
+ if self.path != "/v1/chat/completions": self.send_error(404); return
97
+ ln = int(self.headers.get("Content-Length","0"))
98
+ body = self.rfile.read(ln)
99
+ try: req = json.loads(body.decode("utf-8","replace"))
100
+ except: req = {}
101
+ msgs = req.get("messages") or []
102
+ last = ""
103
+ if msgs:
104
+ c = msgs[-1].get("content")
105
+ last = c if isinstance(c, str) else json.dumps(c)
106
+ tool_calls = None; content = None
107
+ m = re.search(r"__TOOL__:([A-Za-z0-9_]+):(.*)", last, re.DOTALL)
108
+ if m:
109
+ tname, rest = m.group(1), m.group(2).strip()
110
+ try: args = json.loads(rest)
111
+ except: args = {"raw": rest}
112
+ tool_calls = [tool_resp(tname, args)]
113
+ elif "__ASSIST_TEXT__:" in last:
114
+ content = last.split("__ASSIST_TEXT__:",1)[1].strip()
115
+ if tool_calls is None and content is None: content = "(mock) no directive"
116
+ msg = {"role":"assistant"}
117
+ if content is not None: msg["content"] = content
118
+ if tool_calls is not None: msg["tool_calls"] = tool_calls
119
+ # Return SSE streaming format (ona always sends stream:true)
120
+ self.send_response(200)
121
+ self.send_header("Content-Type","text/event-stream")
122
+ self.send_header("Cache-Control","no-cache")
123
+ self.end_headers()
124
+ # First chunk: role
125
+ d0 = {"id":"chatcmpl-mock","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":None}]}
126
+ self.wfile.write(("data: "+json.dumps(d0)+"\n\n").encode())
127
+ # Content or tool_calls chunk
128
+ if content is not None:
129
+ d1 = {"id":"chatcmpl-mock","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":content},"finish_reason":None}]}
130
+ self.wfile.write(("data: "+json.dumps(d1)+"\n\n").encode())
131
+ if tool_calls is not None:
132
+ for i, tc in enumerate(tool_calls):
133
+ d1 = {"id":"chatcmpl-mock","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"tool_calls":[{"index":i,"id":tc["id"],"type":"function","function":{"name":tc["function"]["name"],"arguments":tc["function"]["arguments"]}}]},"finish_reason":None}]}
134
+ self.wfile.write(("data: "+json.dumps(d1)+"\n\n").encode())
135
+ # Final chunk: finish_reason
136
+ d2 = {"id":"chatcmpl-mock","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop" if content else "tool_calls"}]}
137
+ self.wfile.write(("data: "+json.dumps(d2)+"\n\n").encode())
138
+ self.wfile.write(b"data: [DONE]\n\n")
139
+ self.wfile.flush()
140
+
141
+ if __name__ == "__main__":
142
+ HTTPServer(("127.0.0.1", $port), H).serve_forever()
143
+ PY
144
+ python3 "$py" &
145
+ MOCK_PID=$!
146
+ sleep 0.5
147
+ }
148
+
149
+ # Helper: run one tool via --eval, return JSON result
150
+ # --eval creates its own conversation in idle, but Read/Glob/Grep/ToolSearch/etc work in idle.
151
+ # For mutating tools (Write/Edit/Bash), we seed the DB in implement phase first.
152
+ tool_one() {
153
+ local row="$1" tool="$2" args="$3"
154
+ fresh_db "tool_$row"
155
+ # Seed in implement phase with approved plan so planning gate allows mutating tools
156
+ db "UPDATE conversations SET phase='implement' WHERE 1=1"
157
+ local conv_id
158
+ conv_id=$(db "SELECT id FROM conversations LIMIT 1")
159
+ db "INSERT INTO plans(conversation_id,content,hash,status) VALUES ('$conv_id','test plan','abc','approved')"
160
+ # Use --eval which respects the existing DB state; use ACCEPT_TMP as cwd so writes stay in temp dir
161
+ SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" --eval '{"tool":"'"$tool"'","input":'"$args"'}' --cwd "$ACCEPT_TMP" 2>&1 || true
162
+ }
163
+
164
+ export -f ona_pipe db put_effective_json fresh_db
165
+ export ONA ACCEPT_TMP REPO_ROOT
166
+
167
+ echo "=== ona SDLC acceptance tests ==="
168
+ echo "TMP: $ACCEPT_TMP"
169
+
170
+ # ═══ §4 Storage ═══════════════════════════════════════════════
171
+
172
+ echo "[§4 Storage]"
173
+
174
+ row_01() {
175
+ fresh_db row01
176
+ tables=$(db ".tables")
177
+ for t in schema_meta conversations sessions state plans summaries events task_ratings memories transcript_entries hook_invocations tool_permission_log settings_snapshot; do
178
+ echo "$tables" | grep -q "$t" || { echo "missing table: $t"; return 1; }
179
+ done
180
+ }
181
+ run_check ROW-01 row_01
182
+
183
+ row_02() { fresh_db row02; v=$(db "SELECT value FROM schema_meta WHERE key='schema_version'"); test "$v" = "1"; }
184
+ run_check ROW-02 row_02
185
+
186
+ row_03() { fresh_db row03; v=$(db "PRAGMA foreign_keys"); test "$v" = "1"; }
187
+ run_check ROW-03 row_03
188
+
189
+ row_04() { fresh_db row04; v=$(db "PRAGMA journal_mode"); test "$v" = "wal"; }
190
+ run_check ROW-04 row_04
191
+
192
+ row_05() { fresh_db row05; v=$(db "PRAGMA busy_timeout"); test "$v" = "30000"; }
193
+ run_check ROW-05 row_05
194
+
195
+ # ═══ §2.9 REPL commands ══════════════════════════════════════
196
+
197
+ echo "[§2.9 REPL commands]"
198
+
199
+ row_30() {
200
+ fresh_db row30
201
+ out=$(echo "/help" | SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" 2>&1)
202
+ echo "$out" | grep -qi "model" && echo "$out" | grep -qi "login" && echo "$out" | grep -qi "status"
203
+ }
204
+ run_check ROW-30 row_30
205
+
206
+ row_31() {
207
+ fresh_db row31
208
+ out=$(echo "/model" | SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" 2>&1)
209
+ echo "$out" | grep -qi "available\|provider\|lm_studio"
210
+ }
211
+ run_check ROW-31 row_31
212
+
213
+ row_34() {
214
+ fresh_db row34
215
+ write_mock_server 18710
216
+ printf "%s\n" "hello" "/clear" "/exit" | \
217
+ LM_STUDIO_BASE_URL="http://127.0.0.1:18710/v1" LM_STUDIO_MODEL=mock SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" >/dev/null 2>&1 || true
218
+ stop_mock
219
+ cnt=$(db "SELECT COUNT(DISTINCT conversation_id) FROM sessions")
220
+ test "$cnt" -ge 2
221
+ }
222
+ run_check ROW-34 row_34
223
+
224
+ row_36() { fresh_db row36; out=$(echo "/config" | SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" 2>&1); echo "$out" | grep -q "model_config\|provider"; }
225
+ run_check ROW-36 row_36
226
+
227
+ row_37() { fresh_db row37; out=$(echo "/status" | SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" 2>&1); echo "$out" | grep -qi "ok\|kind\|source\|auth"; }
228
+ run_check ROW-37 row_37
229
+
230
+ row_38() { fresh_db row38; echo "/exit" | SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" >/dev/null 2>&1; }
231
+ run_check ROW-38 row_38
232
+
233
+ # ═══ §5 Hook plane ═══════════════════════════════════════════
234
+
235
+ echo "[§5 Hooks]"
236
+
237
+ row_40() { node "$REPO_ROOT/scripts/verify-sdlc-hook-order.mjs"; }
238
+ run_check ROW-40 row_40
239
+
240
+ row_49() {
241
+ fresh_db row49
242
+ put_effective_json '{"hooks":[{"hook_event_name":"SessionStart","matcher":"*","command":"echo hooked"}]}'
243
+ echo "/exit" | SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" >/dev/null 2>&1 || true
244
+ cnt=$(db "SELECT COUNT(*) FROM hook_invocations")
245
+ test "$cnt" = "0"
246
+ }
247
+ run_check ROW-49 row_49
248
+
249
+ # ═══ §5.12 Permissions ═══════════════════════════════════════
250
+
251
+ echo "[§5.12 Permissions]"
252
+
253
+ row_50() {
254
+ node --input-type=module -e "
255
+ import { evaluatePermission } from './lib/permissions.mjs'
256
+ const p = { defaultMode: 'default', deny: ['Bash'], allow: ['Read'] }
257
+ if (evaluatePermission(p, 'Bash', {}) !== 'deny') process.exit(1)
258
+ if (evaluatePermission(p, 'Read', {}) !== 'allow') process.exit(1)
259
+ if (evaluatePermission(p, 'Write', {}) !== 'ask') process.exit(1)
260
+ "
261
+ }
262
+ run_check ROW-50 row_50
263
+
264
+ row_51() {
265
+ node --input-type=module -e "
266
+ import { evaluatePermission } from './lib/permissions.mjs'
267
+ if (evaluatePermission({ defaultMode: 'bypassPermissions' }, 'Bash', {}) !== 'allow') process.exit(1)
268
+ "
269
+ }
270
+ run_check ROW-51 row_51
271
+
272
+ row_52() {
273
+ node --input-type=module -e "
274
+ import { evaluatePermission } from './lib/permissions.mjs'
275
+ if (evaluatePermission({ defaultMode: 'plan' }, 'Write', {}) !== 'deny') process.exit(1)
276
+ "
277
+ }
278
+ run_check ROW-52 row_52
279
+
280
+ # ═══ §7 Tools ═════════════════════════════════════════════════
281
+
282
+ echo "[§7 Tools]"
283
+
284
+ write_mock_server 18766
285
+
286
+ row_60() {
287
+ echo "test_read_content" > "$ACCEPT_TMP/tread.txt"
288
+ pj=$(tool_one 60 Read '{"file_path":"'"$ACCEPT_TMP/tread.txt"'"}')
289
+ echo "$pj" | grep -q '"is_error":false' && echo "$pj" | grep -q "test_read_content"
290
+ }
291
+ run_check ROW-60 row_60
292
+
293
+ row_61() {
294
+ pj=$(tool_one 61 Read '{"file_path": "'"$ACCEPT_TMP/nonexistent_xyz"'"}')
295
+ echo "$pj" | grep -q '"is_error":true'
296
+ }
297
+ run_check ROW-61 row_61
298
+
299
+ row_62() {
300
+ pj=$(tool_one 62 Write '{"file_path": "'"$ACCEPT_TMP/twrite.txt"'", "content": "written_by_test"}')
301
+ echo "$pj" | grep -q '"is_error":false'
302
+ grep -q "written_by_test" "$ACCEPT_TMP/twrite.txt"
303
+ }
304
+ run_check ROW-62 row_62
305
+
306
+ row_63() {
307
+ echo "old_text_here" > "$ACCEPT_TMP/tedit.txt"
308
+ pj=$(tool_one 63 Edit '{"file_path": "'"$ACCEPT_TMP/tedit.txt"'", "old_string": "old_text_here", "new_string": "new_text_here"}')
309
+ echo "$pj" | grep -q '"is_error":false'
310
+ grep -q "new_text_here" "$ACCEPT_TMP/tedit.txt"
311
+ }
312
+ run_check ROW-63 row_63
313
+
314
+ row_64() {
315
+ mkdir -p "$ACCEPT_TMP/globdir"
316
+ touch "$ACCEPT_TMP/globdir/a.txt" "$ACCEPT_TMP/globdir/b.js"
317
+ pj=$(tool_one 64 Glob '{"pattern": "*.txt", "path": "'"$ACCEPT_TMP/globdir"'"}')
318
+ echo "$pj" | grep -q "a.txt"
319
+ }
320
+ run_check ROW-64 row_64
321
+
322
+ row_65() {
323
+ echo "findme_unique_string" > "$ACCEPT_TMP/grepfile.txt"
324
+ pj=$(tool_one 65 Grep '{"pattern": "findme_unique_string", "path": "'"$ACCEPT_TMP/grepfile.txt"'"}')
325
+ echo "$pj" | grep -q "grepfile"
326
+ }
327
+ run_check ROW-65 row_65
328
+
329
+ row_67() {
330
+ pj=$(tool_one 67 Bash '{"command": "echo hello_ona_acceptance"}')
331
+ echo "$pj" | grep -q "hello_ona_acceptance" && echo "$pj" | grep -q '"is_error":false'
332
+ }
333
+ run_check ROW-67 row_67
334
+
335
+ row_68() {
336
+ pj=$(tool_one 68 Bash '{"command": "exit 42"}')
337
+ echo "$pj" | grep -q '"is_error":true'
338
+ }
339
+ run_check ROW-68 row_68
340
+
341
+ row_74() {
342
+ # EnterPlanMode via --eval
343
+ fresh_db row74
344
+ pj=$(SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" --eval '{"tool":"EnterPlanMode","input":{}}' 2>&1 || true)
345
+ echo "$pj" | grep -q '"is_error":false'
346
+ # Check that at least one conversation is in planning (--eval creates its own)
347
+ cnt=$(db "SELECT COUNT(*) FROM conversations WHERE phase='planning'")
348
+ test "$cnt" -ge 1
349
+ }
350
+ run_check ROW-74 row_74
351
+
352
+ row_83() {
353
+ pj=$(tool_one 83 Skill '{"skill": "help"}')
354
+ echo "$pj" | grep -q '"is_error":false'
355
+ }
356
+ run_check ROW-83 row_83
357
+
358
+ row_84() {
359
+ pj=$(tool_one 84 ToolSearch '{"query": "Read"}')
360
+ echo "$pj" | grep -qi "read"
361
+ }
362
+ run_check ROW-84 row_84
363
+
364
+ row_85() {
365
+ pj=$(tool_one 85 ListMcpResources '{}')
366
+ echo "$pj" | grep -q '"is_error":false'
367
+ }
368
+ run_check ROW-85 row_85
369
+
370
+ stop_mock
371
+
372
+ # ═══ §8 Workflow ══════════════════════════════════════════════
373
+
374
+ echo "[§8 Workflow]"
375
+
376
+ row_91() {
377
+ fresh_db row91
378
+ db "UPDATE conversations SET phase='implement' WHERE 1=1"
379
+ local conv_id
380
+ conv_id=$(db "SELECT id FROM conversations LIMIT 1")
381
+ node "$ONA" --transition verify --conversation "$conv_id" 2>&1 && return 1 || true
382
+ phase=$(db "SELECT phase FROM conversations WHERE id='$conv_id'")
383
+ test "$phase" = "implement"
384
+ }
385
+ run_check ROW-91 row_91
386
+
387
+ row_92() {
388
+ fresh_db row92
389
+ db "UPDATE conversations SET phase='implement' WHERE 1=1"
390
+ local conv_id
391
+ conv_id=$(db "SELECT id FROM conversations LIMIT 1")
392
+ node "$ONA" --transition test --conversation "$conv_id" >/dev/null 2>&1
393
+ phase=$(db "SELECT phase FROM conversations WHERE id='$conv_id'")
394
+ test "$phase" = "test"
395
+ }
396
+ run_check ROW-92 row_92
397
+
398
+ row_95() {
399
+ # §8.3 Planning gate blocks Write ONLY in planning phase without an approved plan
400
+ fresh_db row95
401
+ # In idle phase, Write should NOT be blocked by planning gate
402
+ pj=$(SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" --eval '{"tool":"Write","input":{"file_path":"'"$ACCEPT_TMP/sdlc_block_95.txt"'","content":"x"}}' 2>&1 || true)
403
+ # Should succeed (not denied by planning gate) — may be denied by permissions but not §8.3
404
+ # Transition to planning phase and verify Write IS blocked there
405
+ fresh_db row95b
406
+ CONV=$(db "SELECT id FROM conversations LIMIT 1")
407
+ db "UPDATE conversations SET phase='planning' WHERE id='$CONV'"
408
+ pj2=$(SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" --eval '{"tool":"Write","input":{"file_path":"'"$ACCEPT_TMP/sdlc_block_95b.txt"'","content":"x"}}' 2>&1 || true)
409
+ echo "$pj2" | grep -q "denied\|SDLC\|planning"
410
+ }
411
+ run_check ROW-95 row_95
412
+
413
+ # ═══ §0 Forbidden ═════════════════════════════════════════════
414
+
415
+ echo "[§0 Forbidden patterns]"
416
+
417
+ row_100() {
418
+ for tool in Read Glob Grep Brief TodoWrite TaskOutput TaskStop EnterPlanMode Skill ToolSearch ListMcpResources ReadMcpResource; do
419
+ pj=$(tool_one "100_$tool" "$tool" '{}')
420
+ if echo "$pj" | grep -qi "not implemented"; then echo "STUB: $tool"; return 1; fi
421
+ done
422
+ }
423
+ run_check ROW-100 row_100
424
+
425
+ # ═══ §13 Payloads ═════════════════════════════════════════════
426
+
427
+ echo "[§13 Payloads]"
428
+
429
+ row_110() {
430
+ write_mock_server 18810
431
+ fresh_db row110
432
+ printf "%s\n" "__ASSIST_TEXT__:hello" "/exit" | \
433
+ LM_STUDIO_BASE_URL="http://127.0.0.1:18810/v1" LM_STUDIO_MODEL=mock SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" >/dev/null 2>&1 || true
434
+ stop_mock
435
+ pj=$(db "SELECT payload_json FROM transcript_entries WHERE entry_type='user' LIMIT 1")
436
+ echo "$pj" | grep -q "_t.*user"
437
+ }
438
+ run_check ROW-110 row_110
439
+
440
+ row_111() {
441
+ write_mock_server 18811
442
+ fresh_db row111
443
+ printf "%s\n" "__ASSIST_TEXT__:test" "/exit" | \
444
+ LM_STUDIO_BASE_URL="http://127.0.0.1:18811/v1" LM_STUDIO_MODEL=mock SDLC_DISABLE_ALL_HOOKS=1 node "$ONA" >/dev/null 2>&1 || true
445
+ stop_mock
446
+ pj=$(db "SELECT payload_json FROM transcript_entries WHERE entry_type='assistant' LIMIT 1")
447
+ echo "$pj" | grep -q "_t.*assistant"
448
+ }
449
+ run_check ROW-111 row_111
450
+
451
+ # ═══ §F.2 Coverage Validation ═════════════════════════════════
452
+
453
+ echo "[§F.2 Coverage Validation]"
454
+
455
+ # Run coverage validation to ensure all normative requirements have passing tests
456
+ COVERAGE_VALIDATOR="$REPO_ROOT/tests/spec-behavioral/coverage/validate-coverage.sh"
457
+ if [[ -f "$COVERAGE_VALIDATOR" ]]; then
458
+ if "$COVERAGE_VALIDATOR" --format text >/dev/null 2>&1; then
459
+ PASS=$((PASS + 1))
460
+ echo " PASS: Coverage validation"
461
+ else
462
+ echo " FAIL: Coverage validation" >&2
463
+ exit 1
464
+ fi
465
+ TOTAL=$((TOTAL + 1))
466
+ else
467
+ echo " SKIP: Coverage validator not found" >&2
468
+ SKIP=$((SKIP + 1))
469
+ TOTAL=$((TOTAL + 1))
470
+ fi
471
+
472
+ # ═══ Summary ══════════════════════════════════════════════════════
473
+
474
+ echo ""
475
+ echo "=== Results: $PASS passed, $SKIP skipped, $((TOTAL - PASS - SKIP)) failed out of $TOTAL ==="
476
+ if [[ $((TOTAL - PASS - SKIP)) -gt 0 ]]; then exit 1; fi
477
+ echo "ALL PASS"
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { HOOK_INPUT_SCHEMA_UNION_ORDER } from '../lib/hookShapeOrder.mjs'
3
+
4
+ const SPEC_ORDER = [
5
+ 'PreToolUse', 'PostToolUse', 'PostToolUseFailure', 'PermissionDenied',
6
+ 'Notification', 'UserPromptSubmit', 'SessionStart', 'SessionEnd',
7
+ 'Stop', 'StopFailure', 'SubagentStart', 'SubagentStop',
8
+ 'PreCompact', 'PostCompact', 'PermissionRequest', 'Setup',
9
+ 'TeammateIdle', 'TaskCreated', 'TaskCompleted',
10
+ 'Elicitation', 'ElicitationResult', 'ConfigChange', 'InstructionsLoaded',
11
+ 'WorktreeCreate', 'WorktreeRemove', 'CwdChanged', 'FileChanged',
12
+ ]
13
+
14
+ if (HOOK_INPUT_SCHEMA_UNION_ORDER.length !== SPEC_ORDER.length) {
15
+ console.error(`Length mismatch: runtime=${HOOK_INPUT_SCHEMA_UNION_ORDER.length} spec=${SPEC_ORDER.length}`)
16
+ process.exit(1)
17
+ }
18
+
19
+ for (let i = 0; i < SPEC_ORDER.length; i++) {
20
+ if (HOOK_INPUT_SCHEMA_UNION_ORDER[i] !== SPEC_ORDER[i]) {
21
+ console.error(`Mismatch at ${i}: runtime=${HOOK_INPUT_SCHEMA_UNION_ORDER[i]} spec=${SPEC_ORDER[i]}`)
22
+ process.exit(1)
23
+ }
24
+ }
25
+
26
+ console.log(`verify-sdlc-hook-order: OK (${SPEC_ORDER.length} union members)`)
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # TEMPLATE: e2e_workflow
4
+ # PLAN_REQ: <filled by generator — exact text from plan success criteria>
5
+ # SURFACE: db_state
6
+
7
+ export AGENT_SDLC_DB="${AGENT_SDLC_DB:-/tmp/sdlc_test_$$.db}"
8
+ trap 'rm -f "$AGENT_SDLC_DB" "$AGENT_SDLC_DB-wal" "$AGENT_SDLC_DB-shm"' EXIT
9
+
10
+ # ══ SETUP ══
11
+ # <filled by generator — initialize clean DB>
12
+ # ona --init-db
13
+
14
+ # ══ EXERCISE ══
15
+ # <filled by generator — step through workflow phases via CLI>
16
+ # ona --enter-plan --conversation <CONV_ID>
17
+ # ona --approve-plan --conversation <CONV_ID>
18
+ # ona --transition implement --conversation <CONV_ID>
19
+ # ona --transition test --conversation <CONV_ID>
20
+ # ona --transition verify --conversation <CONV_ID>
21
+ # ona --transition done --conversation <CONV_ID>
22
+
23
+ # ══ ASSERT ══
24
+ # <filled by generator — verify final and intermediate phases>
25
+ # PHASE=$(sqlite3 "$AGENT_SDLC_DB" \
26
+ # "SELECT phase FROM conversations ORDER BY created_at DESC LIMIT 1")
27
+ # test "$PHASE" = "<EXPECTED_FINAL_PHASE>" || { echo "FAIL: expected <EXPECTED>, got $PHASE"; exit 1; }
28
+ #
29
+ # PHASES=$(sqlite3 "$AGENT_SDLC_DB" \
30
+ # "SELECT detail FROM events WHERE event_type='phase' ORDER BY id")
31
+ # echo "$PHASES" | grep 'planning' || { echo "FAIL: missing planning phase"; exit 1; }
32
+ # echo "$PHASES" | grep 'implement' || { echo "FAIL: missing implement phase"; exit 1; }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # TEMPLATE: file_state
4
+ # PLAN_REQ: <filled from Given/When/Then criterion>
5
+ # SURFACE: file_state
6
+
7
+ # ══ SETUP ══
8
+ # <from Given — create input fixtures>
9
+
10
+ # ══ EXERCISE ══
11
+ # <from When — run the implementation that produces output files>
12
+
13
+ # ══ ASSERT ══
14
+ # <from Then — check files exist and contain expected content with test -f/grep/diff>
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # TEMPLATE: hook_contract
4
+ # PLAN_REQ: <filled by generator — exact text from plan success criteria>
5
+ # SURFACE: hook_record
6
+
7
+ export AGENT_SDLC_DB="${AGENT_SDLC_DB:-/tmp/sdlc_test_$$.db}"
8
+ trap 'rm -f "$AGENT_SDLC_DB" "$AGENT_SDLC_DB-wal" "$AGENT_SDLC_DB-shm"' EXIT
9
+
10
+ # ══ SETUP ══
11
+ # <filled by generator — configure hook via settings_snapshot>
12
+ # ona --init-db
13
+ # sqlite3 "$AGENT_SDLC_DB" \
14
+ # "INSERT OR REPLACE INTO settings_snapshot(scope, json, updated_at)
15
+ # VALUES ('effective',
16
+ # '{\"hooks\":[{\"hook_event_name\":\"<EVENT>\",\"matcher\":\"<MATCHER>\",\"command\":\"<COMMAND>\"}]}',
17
+ # datetime('now'))"
18
+
19
+ # ══ EXERCISE ══
20
+ # <filled by generator — trigger the hook event via ona CLI>
21
+
22
+ # ══ ASSERT ══
23
+ # <filled by generator — verify hook_invocations row>
24
+ # ROW=$(sqlite3 "$AGENT_SDLC_DB" \
25
+ # "SELECT exit_code FROM hook_invocations
26
+ # WHERE hook_event='<EVENT>' ORDER BY id DESC LIMIT 1")
27
+ # test "$ROW" = "<EXPECTED_EXIT_CODE>" || { echo "FAIL: exit_code=$ROW, expected <EXPECTED>"; exit 1; }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # TEMPLATE: http_response
4
+ # PLAN_REQ: <filled from Given/When/Then criterion>
5
+ # SURFACE: http_response
6
+
7
+ # ══ SETUP ══
8
+ # <from Given — start server, seed data>
9
+
10
+ # ══ EXERCISE ══
11
+ # <from When — curl/wget the endpoint>
12
+
13
+ # ══ ASSERT ══
14
+ # <from Then — check HTTP status, response body, headers>
15
+
16
+ # ══ TEARDOWN ══
17
+ # <kill server process>
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # TEMPLATE: phase_transition
4
+ # PLAN_REQ: <filled by generator — exact text from plan success criteria>
5
+ # SURFACE: db_state
6
+
7
+ export AGENT_SDLC_DB="${AGENT_SDLC_DB:-/tmp/sdlc_test_$$.db}"
8
+ trap 'rm -f "$AGENT_SDLC_DB" "$AGENT_SDLC_DB-wal" "$AGENT_SDLC_DB-shm"' EXIT
9
+
10
+ # ══ SETUP ══
11
+ # <filled by generator — initialize DB and seed conversation at starting phase>
12
+ # ona --init-db
13
+ # sqlite3 "$AGENT_SDLC_DB" \
14
+ # "INSERT INTO conversations(id, project_dir, phase) VALUES ('<CONV_ID>', '/tmp', '<FROM_PHASE>')"
15
+
16
+ # ══ EXERCISE ══
17
+ # <filled by generator — attempt phase transition via CLI>
18
+ # ona --transition <TO_PHASE> --conversation <CONV_ID> 2>&1; TRANSITION_EXIT=$?
19
+
20
+ # ══ ASSERT ══
21
+ # <filled by generator — verify phase changed or was blocked>
22
+ # PHASE=$(sqlite3 "$AGENT_SDLC_DB" "SELECT phase FROM conversations WHERE id='<CONV_ID>'")
23
+ # test "$PHASE" = "<EXPECTED_PHASE>" || { echo "FAIL: expected <EXPECTED_PHASE>, got $PHASE"; exit 1; }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # TEMPLATE: process_output
4
+ # PLAN_REQ: <filled from Given/When/Then criterion>
5
+ # SURFACE: process_output
6
+
7
+ # ══ SETUP ══
8
+ # <from Given — create fixtures, install deps, set env>
9
+
10
+ # ══ EXERCISE ══
11
+ # <from When — run the command/script through its public interface>
12
+
13
+ # ══ ASSERT ══
14
+ # <from Then — check stdout/stderr/exit code with grep/test>
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # TEMPLATE: tool_contract
4
+ # PLAN_REQ: <filled by generator — exact text from plan success criteria>
5
+ # SURFACE: tool_result
6
+
7
+ export AGENT_SDLC_DB="${AGENT_SDLC_DB:-/tmp/sdlc_test_$$.db}"
8
+ trap 'rm -f "$AGENT_SDLC_DB" "$AGENT_SDLC_DB-wal" "$AGENT_SDLC_DB-shm"' EXIT
9
+
10
+ # ══ SETUP ══
11
+ # <filled by generator — create input fixtures: files, directories, env vars>
12
+ # Example: echo -e "line1\nline2" > /tmp/sdlc_test_input.txt
13
+
14
+ # ══ EXERCISE ══
15
+ # <filled by generator — invoke tool through ona CLI>
16
+ # ona --eval '{"tool": "<TOOL_NAME>", "input": {<TOOL_INPUT_JSON>}}'
17
+
18
+ # ══ ASSERT ══
19
+ # <filled by generator — check tool_result in transcript_entries>
20
+ # RESULT=$(sqlite3 "$AGENT_SDLC_DB" \
21
+ # "SELECT payload_json FROM transcript_entries
22
+ # WHERE entry_type='tool_result' ORDER BY sequence DESC LIMIT 1")
23
+ # echo "$RESULT" | grep '"is_error":false' || { echo "FAIL: unexpected is_error"; exit 1; }
24
+ # echo "$RESULT" | grep '<EXPECTED_PATTERN>' || { echo "FAIL: content mismatch"; exit 1; }