@asobacloud/nehanda 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/LICENSE.md +57 -0
- package/README.md +404 -2
- package/bin/agent.mjs +1260 -0
- package/bin/nehanda-ui.mjs +824 -0
- package/lib/anthropicHeaders.mjs +8 -0
- package/lib/app.mjs +313 -0
- package/lib/auth.mjs +255 -0
- package/lib/bashguard.mjs +159 -0
- package/lib/compact.mjs +306 -0
- package/lib/energy-middleware.mjs +318 -0
- package/lib/hookShapeOrder.mjs +30 -0
- package/lib/hookplane.mjs +228 -0
- package/lib/instructions.mjs +23 -0
- package/lib/jev_read.mjs +216 -0
- package/lib/jev_write.mjs +253 -0
- package/lib/laya.mjs +296 -0
- package/lib/laya_sidecar.mjs +198 -0
- package/lib/mcp-config.mjs +189 -0
- package/lib/mcp.mjs +169 -0
- package/lib/modelConfig.mjs +152 -0
- package/lib/modelDiscovery.mjs +205 -0
- package/lib/openaiCompat.mjs +147 -0
- package/lib/orchestrate.mjs +1163 -0
- package/lib/paths.mjs +20 -0
- package/lib/permissions.mjs +44 -0
- package/lib/scripts/__pycache__/laya_worker.cpython-314.pyc +0 -0
- package/lib/scripts/audit-code-integrity.py +437 -0
- package/lib/scripts/ensure-laya-env.sh +55 -0
- package/lib/scripts/js-safety-checker.cjs +308 -0
- package/lib/scripts/laya_worker.py +171 -0
- package/lib/scripts/odse-transform.py +97 -0
- package/lib/scripts/python-safety-checker.py +197 -0
- package/lib/scripts/shell-safety-checker.sh +306 -0
- package/lib/settings.mjs +147 -0
- package/lib/store.mjs +278 -0
- package/lib/team.mjs +69 -0
- package/lib/testgen.mjs +146 -0
- package/lib/tools/AuditCodeIntegrity.json +30 -0
- package/lib/tools/JsSafetyChecker.json +29 -0
- package/lib/tools/PythonSafetyChecker.json +29 -0
- package/lib/tools/ShellSafetyChecker.json +29 -0
- package/lib/tools.mjs +907 -0
- package/lib/transcript.mjs +223 -0
- package/lib/ui.mjs +286 -0
- package/lib/workflow.mjs +91 -0
- package/lib/worktree.mjs +56 -0
- package/package.json +50 -4
- package/schema.sql +181 -0
- package/scripts/postinstall.mjs +62 -0
- package/scripts/sdlc-acceptance.sh +477 -0
- package/scripts/verify-sdlc-hook-order.mjs +26 -0
- package/templates/test_e2e.sh +32 -0
- package/templates/test_file.sh +14 -0
- package/templates/test_hook.sh +27 -0
- package/templates/test_http.sh +17 -0
- package/templates/test_phase.sh +23 -0
- package/templates/test_process.sh +14 -0
- package/templates/test_tool.sh +24 -0
- 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; }
|