loki-mode 7.121.3 → 7.121.4

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/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.121.3
6
+ # Loki Mode v7.121.4
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
408
408
 
409
409
  ---
410
410
 
411
- **v7.121.3 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.121.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.121.3
1
+ 7.121.4
@@ -0,0 +1,83 @@
1
+ # contract-scaffold (M1: the contract-first codegen spine)
2
+
3
+ The single load-bearing mechanism behind "a real, wired app, not a static shell."
4
+ See `artifacts/beat-replit-engineering-plan.md` (studied from Replit's Adopt) and
5
+ `artifacts/replit-adopt-study.md`.
6
+
7
+ ## What it does
8
+
9
+ `scaffold.sh <out_dir> <resource> [field:type ...]` emits a **contract-first**
10
+ project skeleton:
11
+
12
+ 1. An **OpenAPI contract** (`openapi.yaml`) as a first-class artifact -- the single
13
+ source of truth for the API.
14
+ 2. An **Orval config** that generates typed react-query hooks FROM the contract.
15
+ 3. `package.json` + `tsconfig.json` with a real codegen + typecheck script.
16
+
17
+ The point: after `npm run codegen`, a page can only call hooks that the contract
18
+ defines. A page referencing an endpoint the backend does not implement **fails
19
+ typecheck** -- so a static-shell-passed-off-as-wired becomes impossible.
20
+
21
+ ## Proven (tests/test-contract-scaffold.sh, 7/7)
22
+
23
+ - scaffold -> real `openapi.yaml`
24
+ - real Orval codegen -> `useListBookmark` / `useCreateBookmark` / `useDeleteBookmark`
25
+ - a page on a REAL endpoint typechecks
26
+ - a page on a NON-CONTRACT endpoint **fails typecheck** (drift blocked)
27
+
28
+ Proven on a throwaway `bookmark` resource -- **general by construction, no
29
+ knowledge of any specific PRD** (anti-teaching-to-the-test).
30
+
31
+ ## Status: STANDALONE + ADDITIVE -- wired into NO build lane
32
+
33
+ This is a capability module, like FV-1 was. It changes zero existing build
34
+ behavior. **Wiring it into the default build lane is the gated next step** (the
35
+ M1->"M1-wire" fork, analogous to FV-1->FV-2): that step touches the parity-locked
36
+ `run.sh`/`build_prompt.ts` core, reclassifies what every build produces, and
37
+ requires bash+bun parity work + council review + founder sign-off. Do NOT wire it
38
+ in as a solo/rushed action.
39
+
40
+ ## Scope
41
+
42
+ Gap A (the generated app's own quality). NOT Gap B (Replit's managed cloud:
43
+ hosted DB provisioning, auth, secrets, deploy infra -- multi-quarter, out).
44
+
45
+ ## M2 -- real backend floor (DONE, standalone)
46
+
47
+ `generate.mjs <out_dir> <resource> [field:type ...]` produces a REAL Express +
48
+ SQLite backend (via `templates/server/`, substitution NOT heredocs) that persists:
49
+ POST creates a row that survives to a later GET, DELETE removes it, one seed row so
50
+ the first screen is never blank. Every declared field is always bound (a partial
51
+ POST persists cleanly, 201, never 500 -- a bug the FV harness caught).
52
+
53
+ Proven (tests/test-backend-floor.sh, 6/6) on a throwaway `note` resource: generates,
54
+ starts, POST persists, partial POST persists, and **FV-1 reports
55
+ functional_status=verified** -- the convergence. A static shell would fail this.
56
+
57
+ Architecture: template files + a Node substitutor, NOT bash heredocs emitting JS
58
+ (heredocs collide with JS `${...}`/backticks -- learned the hard way; see the plan).
59
+
60
+ ## M3 -- design-system pass (DONE, standalone)
61
+
62
+ `generate.mjs` also emits (into `src/`, from `templates/ui/`) a design system so a
63
+ generated frontend looks DESIGNED, not default:
64
+ - `theme.css` -- ROLE tokens (bg/surface/text/primary/...), a confident modern
65
+ default palette, and a parallel dark theme honored from the OS (`--no-ui` skips).
66
+ - `<Resource>ListView.tsx` -- a data surface with all THREE UI states: a shimmer
67
+ skeleton (never a blank flash), an inviting empty state with a CTA, and a
68
+ first-run "you're viewing sample data" banner (backed by M2's seed row). Wired
69
+ to the M1 contract hook, so it cannot reference a non-contract endpoint.
70
+
71
+ Small touches that make users smile: friendly copy, a clear primary action,
72
+ graceful loading/empty/error states, no raw hex (re-theme from one block).
73
+
74
+ Proven (tests/test-design-system.sh, 8/8): no raw hex in components, all three
75
+ states, dark theme, AND the tokenized ListView TYPECHECKS against the generated
76
+ contract hooks (the contract even caught a wrong response-type assumption in the
77
+ UI -- the spine working as designed).
78
+
79
+ ## Next (wiring, see the plan)
80
+
81
+ - Wire M1+M2+M3 into the build lane (GATED: touches parity-locked run.sh/
82
+ build_prompt.ts core -> bash+bun parity + council + founder sign-off).
83
+ - Wire the FV harness into the completion verdict (FV-2, founder-gated).
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // generate.mjs -- M2 backend-floor generator (template substitution, NOT heredocs).
3
+ //
4
+ // Reads the server template files and substitutes tokens derived from a portable
5
+ // {resource, fields} descriptor, producing a REAL Express + SQLite backend that
6
+ // persists. General by construction: no knowledge of any specific PRD.
7
+ //
8
+ // Why a Node generator and not a bash heredoc: a heredoc emitting JS collides with
9
+ // JS `${...}` template literals and backticks (bit both the M1 yaml and a first
10
+ // M2 js heredoc). Templates + substitution keeps generated code reviewable as real
11
+ // code. See artifacts/beat-replit-engineering-plan.md (M2 architecture decision).
12
+ //
13
+ // Usage: node generate.mjs <out_dir> <resource> [field:type ...]
14
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const __dir = dirname(fileURLToPath(import.meta.url));
19
+ const TPL = join(__dir, "templates", "server");
20
+
21
+ const [, , outDir, resource, ...fieldArgs] = process.argv;
22
+ if (!outDir || !resource) {
23
+ console.error("usage: generate.mjs <out_dir> <resource> [field:type ...]");
24
+ process.exit(2);
25
+ }
26
+
27
+ const fields = (fieldArgs.length ? fieldArgs : ["title:string"]).map((f) => {
28
+ const [name, type = "string"] = f.split(":");
29
+ return { name, type };
30
+ });
31
+
32
+ const cap = resource.charAt(0).toUpperCase() + resource.slice(1);
33
+ const coll = resource.endsWith("s") ? resource : resource + "s";
34
+
35
+ const sqlType = (t) =>
36
+ ["int", "integer", "number", "bool", "boolean"].includes(t) ? "INTEGER" : "TEXT";
37
+ const seedVal = (t) =>
38
+ ["int", "integer", "number"].includes(t) ? "1"
39
+ : ["bool", "boolean"].includes(t) ? "0"
40
+ : "'sample value'";
41
+
42
+ const tokens = {
43
+ __COLL__: coll,
44
+ __RESOURCE_CAP__: cap,
45
+ __COLUMNS_SQL__: fields.map((f) => ` ${f.name} ${sqlType(f.type)},`).join("\n"),
46
+ __SEED_COLS__: fields.length ? fields.map((f) => f.name).join(", ") + "," : "",
47
+ __SEED_VALS__: fields.length ? fields.map((f) => seedVal(f.type)).join(", ") + "," : "",
48
+ __INSERT_COLS__: fields.length ? fields.map((f) => f.name).join(", ") + "," : "",
49
+ __INSERT_PLACEHOLDERS__: fields.length ? fields.map((f) => "@" + f.name).join(", ") + "," : "",
50
+ __BODY_FIELDS__: fields.map((f) => `"${f.name}"`).join(", "),
51
+ };
52
+
53
+ function render(tpl) {
54
+ let out = tpl;
55
+ // Replace longer tokens first so __RESOURCE_CAP__ isn't partially hit, etc.
56
+ for (const key of Object.keys(tokens).sort((a, b) => b.length - a.length)) {
57
+ out = out.split(key).join(tokens[key]);
58
+ }
59
+ return out;
60
+ }
61
+
62
+ mkdirSync(join(outDir, "server"), { recursive: true });
63
+ for (const [tpl, dest] of [
64
+ ["index.mjs.tmpl", "index.mjs"],
65
+ ["package.json.tmpl", "package.json"],
66
+ ]) {
67
+ const src = readFileSync(join(TPL, tpl), "utf8");
68
+ writeFileSync(join(outDir, "server", dest), render(src));
69
+ }
70
+
71
+ // M3: the design-system UI templates (tokens + dark + three states), wired to the
72
+ // M1 contract hooks. Emitted into src/ alongside the generated client so a
73
+ // generated frontend looks DESIGNED (not default) and cannot reference a
74
+ // non-contract endpoint. Optional: skip with --no-ui.
75
+ if (!process.argv.includes("--no-ui")) {
76
+ const UI = join(__dir, "templates", "ui");
77
+ mkdirSync(join(outDir, "src"), { recursive: true });
78
+ for (const [tpl, dest] of [
79
+ ["theme.css.tmpl", "theme.css"],
80
+ ["ListView.tsx.tmpl", `${cap}ListView.tsx`],
81
+ ]) {
82
+ const src = readFileSync(join(UI, tpl), "utf8");
83
+ writeFileSync(join(outDir, "src", dest), render(src));
84
+ }
85
+ }
86
+
87
+ console.log(`generated real backend floor at ${outDir}/server (Express + SQLite, /${coll})`);
88
+ if (!process.argv.includes("--no-ui"))
89
+ console.log(`generated design-system UI at ${outDir}/src (tokens + dark + 3 states, wired to the contract)`);
90
+ console.log(`next: (cd ${outDir}/server && npm install && PORT=3000 npm start)`);
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ # contract-scaffold/scaffold.sh -- M1: the contract-first codegen spine.
3
+ #
4
+ # The single load-bearing mechanism behind "a real wired app, not a static shell"
5
+ # (see artifacts/beat-replit-engineering-plan.md, studied from Replit's Adopt).
6
+ # It emits an OpenAPI contract as a first-class artifact, then generates typed
7
+ # client hooks + validators FROM it, so a page PHYSICALLY CANNOT call an endpoint
8
+ # the backend does not implement -- it will not typecheck. A static mock stops
9
+ # being possible to pass off as a wired app.
10
+ #
11
+ # STATUS: STANDALONE + ADDITIVE. Wired into NO build lane. It is a capability
12
+ # module proven on throwaway specs; wiring it into the default build is the
13
+ # gated next step (like FV-1 -> FV-2), requiring bash+bun parity work + council.
14
+ #
15
+ # GENERAL by construction: it takes a portable {name, entity, fields} descriptor
16
+ # and produces a contract-first skeleton for ANY simple REST resource. It has NO
17
+ # knowledge of any specific PRD (anti-teaching-to-the-test).
18
+ #
19
+ # Usage:
20
+ # scaffold.sh <out_dir> <resource_name> [field:type ...]
21
+ # Example:
22
+ # scaffold.sh /tmp/bookmarks bookmark url:string title:string
23
+ set -euo pipefail
24
+
25
+ OUT="${1:?usage: scaffold.sh <out_dir> <resource> [field:type ...]}"
26
+ RES="${2:?resource name required}"
27
+ shift 2 || true
28
+ FIELDS=("$@")
29
+ [ "${#FIELDS[@]}" -eq 0 ] && FIELDS=("title:string")
30
+
31
+ # Plural collection path (naive but sufficient: append 's' unless already plural).
32
+ case "$RES" in *s) COLL="$RES" ;; *) COLL="${RES}s" ;; esac
33
+
34
+ mkdir -p "$OUT"
35
+
36
+ # ---- 1. The OpenAPI contract (the single source of truth) -------------------
37
+ # Build the schema properties + a create-body from the field descriptors.
38
+ _props=""; _required=""; _create_props=""
39
+ for f in "${FIELDS[@]}"; do
40
+ fname="${f%%:*}"; ftype="${f##*:}"
41
+ case "$ftype" in
42
+ int|integer|number) otype="integer" ;;
43
+ bool|boolean) otype="boolean" ;;
44
+ *) otype="string" ;;
45
+ esac
46
+ _props+=" ${fname}: { type: ${otype} }
47
+ "
48
+ _create_props+=" ${fname}: { type: ${otype} }
49
+ "
50
+ _required+=" - ${fname}
51
+ "
52
+ done
53
+
54
+ cat > "$OUT/openapi.yaml" <<YAML
55
+ openapi: 3.1.0
56
+ info: { title: Api, version: 1.0.0 }
57
+ servers: [{ url: /api }]
58
+ paths:
59
+ /${COLL}:
60
+ get:
61
+ operationId: list${RES^}
62
+ responses:
63
+ '200':
64
+ description: OK
65
+ content:
66
+ application/json:
67
+ schema: { type: array, items: { \$ref: '#/components/schemas/${RES^}' } }
68
+ post:
69
+ operationId: create${RES^}
70
+ requestBody:
71
+ required: true
72
+ content:
73
+ application/json:
74
+ schema: { \$ref: '#/components/schemas/${RES^}Create' }
75
+ responses:
76
+ '201':
77
+ description: Created
78
+ content:
79
+ application/json:
80
+ schema: { \$ref: '#/components/schemas/${RES^}' }
81
+ /${COLL}/{id}:
82
+ delete:
83
+ operationId: delete${RES^}
84
+ parameters:
85
+ - { name: id, in: path, required: true, schema: { type: string } }
86
+ responses:
87
+ '204': { description: No Content }
88
+ components:
89
+ schemas:
90
+ ${RES^}:
91
+ type: object
92
+ required:
93
+ - id
94
+ ${_required} properties:
95
+ id: { type: string }
96
+ ${_props} ${RES^}Create:
97
+ type: object
98
+ required:
99
+ ${_required} properties:
100
+ ${_create_props}
101
+ YAML
102
+
103
+ # ---- 2. Orval config: contract -> typed react-query hooks -------------------
104
+ cat > "$OUT/orval.config.ts" <<'TS'
105
+ import { defineConfig } from "orval";
106
+ export default defineConfig({
107
+ client: {
108
+ input: "./openapi.yaml",
109
+ output: {
110
+ target: "./src/generated/api.ts",
111
+ client: "react-query",
112
+ mode: "single",
113
+ clean: true,
114
+ override: { header: () => ["// GENERATED by orval -- do not edit manually."] },
115
+ },
116
+ },
117
+ });
118
+ TS
119
+
120
+ # ---- 3. package.json (real deps, real codegen script) ----------------------
121
+ cat > "$OUT/package.json" <<JSON
122
+ {
123
+ "name": "contract-first-${COLL}",
124
+ "private": true,
125
+ "type": "module",
126
+ "scripts": {
127
+ "codegen": "orval --config ./orval.config.ts",
128
+ "typecheck": "tsc --noEmit"
129
+ },
130
+ "dependencies": { "@tanstack/react-query": "^5.0.0", "react": "^18.0.0" },
131
+ "devDependencies": { "orval": "^8.0.0", "typescript": "^5.4.0", "@types/react": "^18.0.0" }
132
+ }
133
+ JSON
134
+
135
+ cat > "$OUT/tsconfig.json" <<'JSON'
136
+ {
137
+ "compilerOptions": {
138
+ "strict": true, "noEmit": true, "jsx": "react-jsx",
139
+ "module": "ESNext", "moduleResolution": "Bundler",
140
+ "target": "ES2022", "lib": ["ES2022", "DOM"], "skipLibCheck": true
141
+ },
142
+ "include": ["src"]
143
+ }
144
+ JSON
145
+
146
+ echo "scaffolded contract-first skeleton at $OUT (resource=$RES, collection=/$COLL)"
147
+ echo "next: (cd $OUT && npm install && npm run codegen && npm run typecheck)"
@@ -0,0 +1,62 @@
1
+ // Real backend FLOOR: Express + SQLite persistence implementing the M1 contract.
2
+ // POST persists; GET reflects it; DELETE removes it -- a working data pipeline,
3
+ // not a mock. Generated by contract-scaffold (template-substitution, general).
4
+ // (Token reference intentionally omitted from this comment so the substitutor
5
+ // does not rewrite it; see generate.mjs for the token list.)
6
+ import express from "express";
7
+ import Database from "better-sqlite3";
8
+ import { randomUUID } from "node:crypto";
9
+
10
+ const db = new Database(process.env.DB_PATH || ":memory:");
11
+ db.exec(`CREATE TABLE IF NOT EXISTS __COLL__ (
12
+ id TEXT PRIMARY KEY,
13
+ __COLUMNS_SQL__
14
+ created_at TEXT NOT NULL
15
+ );`);
16
+
17
+ // Seed one row so the very first screen is never blank (the empty vs first-run
18
+ // UI-state discipline: a real app greets the user with sample data, not a void).
19
+ const seedCount = db.prepare("SELECT COUNT(*) AS c FROM __COLL__").get().c;
20
+ if (seedCount === 0) {
21
+ db.prepare(
22
+ "INSERT INTO __COLL__ (id, __SEED_COLS__ created_at) VALUES (?, __SEED_VALS__ ?)"
23
+ ).run(randomUUID(), new Date().toISOString());
24
+ }
25
+
26
+ const app = express();
27
+ app.use(express.json());
28
+
29
+ // Only these keys are accepted from a POST body -- never arbitrary columns.
30
+ // EVERY declared field is always bound (defaulting to null when the caller omits
31
+ // it), so a partial POST persists cleanly instead of throwing "missing named
32
+ // parameter". A real app tolerates optional fields; it does not 500 on them.
33
+ const BODY_FIELDS = [__BODY_FIELDS__];
34
+ function pickBody(body) {
35
+ const out = {};
36
+ for (const k of BODY_FIELDS) out[k] = body != null && body[k] !== undefined ? body[k] : null;
37
+ return out;
38
+ }
39
+
40
+ // GET /api/__COLL__ (list__RESOURCE_CAP__)
41
+ app.get("/api/__COLL__", (_req, res) => {
42
+ res.json(db.prepare("SELECT * FROM __COLL__ ORDER BY created_at DESC").all());
43
+ });
44
+
45
+ // POST /api/__COLL__ (create__RESOURCE_CAP__) -- PERSISTS
46
+ app.post("/api/__COLL__", (req, res) => {
47
+ const fields = pickBody(req.body);
48
+ const row = { id: randomUUID(), ...fields, created_at: new Date().toISOString() };
49
+ db.prepare(
50
+ "INSERT INTO __COLL__ (id, __INSERT_COLS__ created_at) VALUES (@id, __INSERT_PLACEHOLDERS__ @created_at)"
51
+ ).run(row);
52
+ res.status(201).json(row);
53
+ });
54
+
55
+ // DELETE /api/__COLL__/:id (delete__RESOURCE_CAP__)
56
+ app.delete("/api/__COLL__/:id", (req, res) => {
57
+ db.prepare("DELETE FROM __COLL__ WHERE id = ?").run(req.params.id);
58
+ res.status(204).end();
59
+ });
60
+
61
+ const port = process.env.PORT ? Number(process.env.PORT) : 3000;
62
+ app.listen(port, () => console.log(`__RESOURCE_CAP__ API listening on port ${port}`));
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "contract-first-__COLL__-server",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": { "start": "node index.mjs" },
6
+ "dependencies": {
7
+ "express": "^5.0.0",
8
+ "better-sqlite3": "^12.0.0"
9
+ }
10
+ }
@@ -0,0 +1,75 @@
1
+ // ListView.tsx -- a data surface that ships all THREE UI states with design
2
+ // tokens, wired to the M1 contract hook. This is what "designed, not default"
3
+ // looks like: skeleton while loading, an inviting empty state with a CTA, a
4
+ // first-run "sample data" banner, and real rows in a tokenized card. It consumes
5
+ // the generated hook, so it physically cannot reference a non-contract endpoint.
6
+ //
7
+ // Generated by contract-scaffold M3 (general: __COLL__/__RESOURCE_CAP__ tokens,
8
+ // no product-specific content). Small touches that make users smile: friendly
9
+ // copy, an icon, a clear primary action, graceful loading + empty states.
10
+ import { useList__RESOURCE_CAP__, useCreate__RESOURCE_CAP__ } from "./generated/api";
11
+
12
+ export function __RESOURCE_CAP__ListView() {
13
+ const { data, isLoading, isError } = useList__RESOURCE_CAP__();
14
+ const create = useCreate__RESOURCE_CAP__();
15
+ // Orval's react-query hook returns the full response object; the array is at
16
+ // .data.data, typed straight from the OpenAPI contract (a real __RESOURCE_CAP__[]).
17
+ // Reading it wrong is a typecheck error -- the contract-first spine catching a
18
+ // drift in the UI itself.
19
+ const items = data?.data ?? [];
20
+ // A seeded backend (M2) means the first real screen shows sample data, not a void.
21
+ const isFirstRun = items.length > 0 && items.length <= 1;
22
+
23
+ // State 1: skeleton -- never a blank flash while loading.
24
+ if (isLoading) {
25
+ return (
26
+ <section className="card" aria-busy="true" aria-label="Loading __COLL__">
27
+ <div className="skeleton" style={{ width: "40%", marginBottom: 12 }} />
28
+ <div className="skeleton" style={{ width: "100%", marginBottom: 8 }} />
29
+ <div className="skeleton" style={{ width: "90%" }} />
30
+ </section>
31
+ );
32
+ }
33
+
34
+ if (isError) {
35
+ return (
36
+ <section className="empty" role="alert">
37
+ <p>We couldn't load your __COLL__. Please try again.</p>
38
+ </section>
39
+ );
40
+ }
41
+
42
+ // State 2: empty -- a dashed invitation with a clear next action, never a void.
43
+ if (items.length === 0) {
44
+ return (
45
+ <section className="empty">
46
+ <h3 style={{ margin: "0 0 4px" }}>No __COLL__ yet</h3>
47
+ <p style={{ margin: "0 0 16px" }}>Get started by creating your first one.</p>
48
+ <button className="btn-primary" onClick={() => create.mutate({ data: {} as never })}>
49
+ Create __RESOURCE_CAP__
50
+ </button>
51
+ </section>
52
+ );
53
+ }
54
+
55
+ // State 3: populated (with a friendly first-run banner when it's sample data).
56
+ return (
57
+ <section>
58
+ {isFirstRun ? (
59
+ <div className="first-run" role="status">
60
+ <span aria-hidden="true">*</span>
61
+ You're viewing sample data. Create a __RESOURCE_CAP__ to make it yours.
62
+ </div>
63
+ ) : null}
64
+ <ul style={{ listStyle: "none", padding: 0, margin: "12px 0 0", display: "grid", gap: 12 }}>
65
+ {items.map((it) => (
66
+ <li key={String((it as { id?: unknown }).id)} className="card">
67
+ <pre style={{ margin: 0, fontFamily: "inherit", whiteSpace: "pre-wrap" }}>
68
+ {JSON.stringify(it, null, 2)}
69
+ </pre>
70
+ </li>
71
+ ))}
72
+ </ul>
73
+ </section>
74
+ );
75
+ }
@@ -0,0 +1,99 @@
1
+ /* theme.css -- design tokens (role-based, NEVER raw hex in components).
2
+ *
3
+ * Why this exists: generated apps look "AI-default" because they scatter raw
4
+ * colors and ship only a happy-path light theme. A real designed app uses ROLE
5
+ * tokens (bg, surface, text, primary, ...) referenced everywhere, ships a
6
+ * parallel dark theme from the same variable block, and honors the OS setting.
7
+ * Swapping one block re-themes the whole app. Modern, high-contrast, accessible.
8
+ *
9
+ * Generated by contract-scaffold M3 (general -- no product-specific colors;
10
+ * a strong neutral+indigo default any greenfield app can ship and re-brand).
11
+ */
12
+ :root {
13
+ /* Surfaces */
14
+ --bg: #f7f8fa;
15
+ --surface: #ffffff;
16
+ --surface-2: #f0f2f5;
17
+ --border: #e3e6eb;
18
+ /* Text (AA contrast on --bg / --surface) */
19
+ --text: #1a1d24;
20
+ --text-muted: #5a6472;
21
+ /* Brand accent -- a confident modern indigo (re-brand by editing here only) */
22
+ --primary: #5b35e8;
23
+ --primary-contrast: #ffffff;
24
+ --primary-weak: #ece8fd;
25
+ /* Semantic */
26
+ --ok: #1f9d55;
27
+ --warn: #b7791f;
28
+ --danger: #c2333a;
29
+ /* Shape + depth (subtle, not shadow-heavy) */
30
+ --radius: 12px;
31
+ --radius-sm: 8px;
32
+ --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.08);
33
+ --space: 16px;
34
+ }
35
+
36
+ /* Dark theme -- same roles, honored automatically from the OS preference. */
37
+ @media (prefers-color-scheme: dark) {
38
+ :root {
39
+ --bg: #0f1117;
40
+ --surface: #171a21;
41
+ --surface-2: #1e222b;
42
+ --border: #2a2f3a;
43
+ --text: #eef1f6;
44
+ --text-muted: #9aa4b2;
45
+ --primary: #8b6dff;
46
+ --primary-contrast: #0f1117;
47
+ --primary-weak: #241f3d;
48
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.5);
49
+ }
50
+ }
51
+ /* Explicit override hook (a theme toggle sets data-theme on <html>). */
52
+ :root[data-theme="dark"] {
53
+ --bg: #0f1117; --surface: #171a21; --surface-2: #1e222b; --border: #2a2f3a;
54
+ --text: #eef1f6; --text-muted: #9aa4b2; --primary: #8b6dff;
55
+ --primary-contrast: #0f1117; --primary-weak: #241f3d;
56
+ }
57
+
58
+ * { box-sizing: border-box; }
59
+ body {
60
+ margin: 0;
61
+ background: var(--bg);
62
+ color: var(--text);
63
+ font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
64
+ -webkit-font-smoothing: antialiased;
65
+ }
66
+ .card {
67
+ background: var(--surface);
68
+ border: 1px solid var(--border);
69
+ border-radius: var(--radius);
70
+ box-shadow: var(--shadow-sm);
71
+ padding: var(--space);
72
+ }
73
+ .btn-primary {
74
+ background: var(--primary); color: var(--primary-contrast);
75
+ border: none; border-radius: var(--radius-sm);
76
+ padding: 8px 16px; font-weight: 600; cursor: pointer;
77
+ }
78
+ .btn-primary:hover { filter: brightness(1.05); }
79
+
80
+ /* --- the three UI states every data surface ships --- */
81
+ /* 1. skeleton (loading) -- a shimmer, never a blank flash */
82
+ .skeleton {
83
+ background: linear-gradient(90deg, var(--surface-2) 25%, var(--border) 37%, var(--surface-2) 63%);
84
+ background-size: 400% 100%;
85
+ animation: shimmer 1.4s ease infinite;
86
+ border-radius: var(--radius-sm); height: 16px;
87
+ }
88
+ @keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
89
+ /* 2. empty state -- a dashed invitation with a CTA, never a void */
90
+ .empty {
91
+ border: 1px dashed var(--border); border-radius: var(--radius);
92
+ padding: calc(var(--space) * 2); text-align: center; color: var(--text-muted);
93
+ }
94
+ /* 3. first-run banner -- "you're viewing sample data" so screen 1 is alive */
95
+ .first-run {
96
+ background: var(--primary-weak); border: 1px solid var(--border);
97
+ border-radius: var(--radius); padding: 12px 16px; color: var(--text);
98
+ display: flex; align-items: center; gap: 8px; font-size: 14px;
99
+ }
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env python3
2
+ """functional-verify.py -- FV-1: does the built app actually DO what the spec asked?
3
+
4
+ The completion verifier proves code was written + tests/build passed. It does NOT
5
+ prove the spec's BEHAVIORS work: a "waitlist" spec can be built as a static page
6
+ that captures nothing, yet read VERIFIED (measured: ~half of backend-implying
7
+ specs). This harness closes that gap by EXERCISING the running app against
8
+ assertions DERIVED FROM THE SPEC, and reporting an honest functional signal.
9
+
10
+ SCOPE (FV-1, deliberately narrow + honest):
11
+ - It reports a DESCRIPTIVE signal only. It does NOT feed the VERIFIED headline
12
+ (that is FV-2: a founder-gated trust-semantics decision, council-reviewed).
13
+ - It derives HTTP endpoint assertions from the spec (GET/POST/DELETE <path>) and
14
+ runs the CRUD lifecycle they imply against the LIVE app. It never fabricates:
15
+ an endpoint it cannot reach is `inconclusive`, a behavior that failed is
16
+ `failed`, a behavior proven is `passed`. Same inconclusive-never-false moat as
17
+ the rest of the trust layer.
18
+ - It requires the app to already be running (URL passed in) -- app-runner (>=
19
+ v7.121.3, static sites included) starts it; this harness only probes.
20
+
21
+ Output: JSON to stdout -- {spec_behaviors: [...], summary: {passed, failed,
22
+ inconclusive}, functional_status: verified|partial|failed|inconclusive}.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import re
28
+ import sys
29
+ import urllib.error
30
+ import urllib.request
31
+
32
+
33
+ # --- spec -> behavioral assertions ------------------------------------------
34
+
35
+ # Endpoint declarations in a spec/PRD: "GET /api/tasks", "POST /api/tasks",
36
+ # "DELETE /api/tasks/:id". Method + path; path may carry a :param placeholder.
37
+ _ENDPOINT_RE = re.compile(
38
+ r'\b(GET|POST|PUT|PATCH|DELETE)\s+(/[A-Za-z0-9_./:{}-]*)',
39
+ )
40
+
41
+
42
+ def _clean_path(path: str) -> str:
43
+ """Strip trailing sentence punctuation a spec sentence leaves on a path
44
+ (e.g. "DELETE /api/tasks/:id." -> "/api/tasks/:id"). Without this, a probe
45
+ hits a wrong URL and fabricates a failure on a working app -- a fake-RED in
46
+ the FV layer itself, exactly what this harness exists to prevent."""
47
+ return path.rstrip(".,;:)]}") or path
48
+
49
+
50
+ def derive_endpoint_assertions(spec_text: str) -> list[dict]:
51
+ """Extract {method, path} endpoint assertions the spec explicitly names.
52
+
53
+ Deduped, order-preserved. A path with a :param / {param} is a resource route
54
+ (used for the DELETE-a-created-resource lifecycle below)."""
55
+ seen = set()
56
+ out = []
57
+ for m in _ENDPOINT_RE.finditer(spec_text or ""):
58
+ method = m.group(1).upper()
59
+ path = _clean_path(m.group(2))
60
+ key = (method, path)
61
+ if key in seen:
62
+ continue
63
+ seen.add(key)
64
+ out.append({"method": method, "path": path,
65
+ "is_resource": bool(re.search(r'[:{]', path))})
66
+ return out
67
+
68
+
69
+ # --- run assertions against the live app ------------------------------------
70
+
71
+ def _req(base_url: str, method: str, path: str, body=None, timeout=8):
72
+ """One HTTP call. Returns (status_code, json_or_text) or (None, error_str)."""
73
+ url = base_url.rstrip("/") + path
74
+ data = None
75
+ headers = {}
76
+ if body is not None:
77
+ data = json.dumps(body).encode()
78
+ headers["Content-Type"] = "application/json"
79
+ req = urllib.request.Request(url, data=data, method=method, headers=headers)
80
+ try:
81
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
82
+ raw = resp.read().decode("utf-8", "replace")
83
+ try:
84
+ return resp.status, json.loads(raw) if raw else None
85
+ except json.JSONDecodeError:
86
+ return resp.status, raw
87
+ except urllib.error.HTTPError as e:
88
+ return e.code, None
89
+ except (urllib.error.URLError, OSError) as e:
90
+ return None, str(e)
91
+
92
+
93
+ def verify_crud_lifecycle(base_url: str, assertions: list[dict]) -> list[dict]:
94
+ """The core functional check: for a REST resource the spec names, prove the
95
+ CRUD LIFECYCLE actually works end to end (not just that a route responds):
96
+ POST creates -> the created item appears on GET -> DELETE removes it ->
97
+ it is gone from a subsequent GET.
98
+ This is what distinguishes a working backend from a static shell: a static
99
+ page returns 200 on GET but a POST does not persist. Each behavior is
100
+ passed / failed / inconclusive, never fabricated."""
101
+ results = []
102
+
103
+ # Collection GET/POST endpoints (non-resource) and their resource DELETE.
104
+ gets = [a for a in assertions if a["method"] == "GET" and not a["is_resource"]]
105
+ posts = [a for a in assertions if a["method"] == "POST" and not a["is_resource"]]
106
+ deletes = [a for a in assertions if a["method"] == "DELETE" and a["is_resource"]]
107
+
108
+ # 1. Every named collection GET must actually respond 2xx.
109
+ for a in gets:
110
+ code, payload = _req(base_url, "GET", a["path"])
111
+ if code is None:
112
+ results.append({"behavior": f"GET {a['path']} responds",
113
+ "status": "inconclusive", "detail": f"unreachable: {payload}"})
114
+ elif 200 <= code < 300:
115
+ results.append({"behavior": f"GET {a['path']} responds",
116
+ "status": "passed", "detail": f"HTTP {code}"})
117
+ else:
118
+ results.append({"behavior": f"GET {a['path']} responds",
119
+ "status": "failed", "detail": f"HTTP {code}"})
120
+
121
+ # 2. POST-creates-and-persists lifecycle: for each collection POST that has a
122
+ # matching collection GET, POST an item and assert the collection grew.
123
+ for p in posts:
124
+ coll = p["path"]
125
+ matching_get = next((g for g in gets if g["path"] == coll), None)
126
+ if matching_get is None:
127
+ results.append({"behavior": f"POST {coll} persists (verifiable)",
128
+ "status": "inconclusive",
129
+ "detail": "no matching collection GET to confirm persistence"})
130
+ continue
131
+ before_code, before = _req(base_url, "GET", coll)
132
+ before_n = len(before) if isinstance(before, list) else None
133
+ # A minimal, generic payload. Real apps validate; we send a common shape.
134
+ post_code, created = _req(base_url, "POST", coll,
135
+ body={"title": "fv-probe", "name": "fv-probe",
136
+ "text": "fv-probe"})
137
+ after_code, after = _req(base_url, "GET", coll)
138
+ after_n = len(after) if isinstance(after, list) else None
139
+ if post_code is None or after_code is None:
140
+ results.append({"behavior": f"POST {coll} persists",
141
+ "status": "inconclusive",
142
+ "detail": "app unreachable during lifecycle"})
143
+ elif post_code and 200 <= post_code < 300 and before_n is not None \
144
+ and after_n is not None and after_n > before_n:
145
+ results.append({"behavior": f"POST {coll} persists",
146
+ "status": "passed",
147
+ "detail": f"collection grew {before_n} -> {after_n} after POST"})
148
+ elif post_code and 200 <= post_code < 300 and (before_n is None or after_n is None):
149
+ # POST accepted but the collection is not a JSON array we can count:
150
+ # we cannot PROVE persistence -> honest inconclusive, never a pass.
151
+ results.append({"behavior": f"POST {coll} persists",
152
+ "status": "inconclusive",
153
+ "detail": f"POST HTTP {post_code} but GET is not a countable list"})
154
+ elif post_code in (400, 422):
155
+ # The endpoint EXISTS and rejected OUR probe payload (validation).
156
+ # That is a working backend with a schema our generic {title,name,text}
157
+ # did not satisfy -- NOT proof the behavior is broken. Marking it failed
158
+ # would be a fake-RED (a strict-but-working app read as non-working).
159
+ # Honest inconclusive; FV-2 must never wire a failed verdict off this.
160
+ results.append({"behavior": f"POST {coll} persists",
161
+ "status": "inconclusive",
162
+ "detail": f"POST HTTP {post_code}: endpoint exists but rejected the "
163
+ "probe payload (schema mismatch, not proof of no-persistence)"})
164
+ else:
165
+ # POST 404 (endpoint absent), 501 (not implemented), or 2xx-but-the-
166
+ # collection did NOT grow -> the behavior the spec implies does NOT
167
+ # work (a static shell or a non-persisting backend). Genuine failed.
168
+ results.append({"behavior": f"POST {coll} persists",
169
+ "status": "failed",
170
+ "detail": f"POST HTTP {post_code}; collection {before_n} -> {after_n} "
171
+ "(no persistence -- a static shell or non-working backend)"})
172
+
173
+ # 3. DELETE lifecycle: if the spec names a resource DELETE and we created
174
+ # an item with an id, delete it and assert it's gone.
175
+ created_id = None
176
+ if isinstance(created, dict):
177
+ created_id = created.get("id") or created.get("_id")
178
+ if deletes and created_id is not None:
179
+ dpath_tmpl = deletes[0]["path"]
180
+ dpath = re.sub(r'[:{][A-Za-z0-9_]+[}]?', str(created_id), dpath_tmpl)
181
+ del_code, _ = _req(base_url, "DELETE", dpath)
182
+ _, after_del = _req(base_url, "GET", coll)
183
+ still_there = isinstance(after_del, list) and any(
184
+ isinstance(x, dict) and (x.get("id") == created_id or x.get("_id") == created_id)
185
+ for x in after_del)
186
+ if del_code is None:
187
+ results.append({"behavior": f"DELETE {dpath_tmpl} removes",
188
+ "status": "inconclusive", "detail": "unreachable"})
189
+ elif 200 <= del_code < 300 and not still_there:
190
+ results.append({"behavior": f"DELETE {dpath_tmpl} removes",
191
+ "status": "passed",
192
+ "detail": f"item {created_id} gone after DELETE (HTTP {del_code})"})
193
+ else:
194
+ results.append({"behavior": f"DELETE {dpath_tmpl} removes",
195
+ "status": "failed",
196
+ "detail": f"DELETE HTTP {del_code}; item still present={still_there}"})
197
+ return results
198
+
199
+
200
+ def summarize(results: list[dict]) -> dict:
201
+ passed = sum(1 for r in results if r["status"] == "passed")
202
+ failed = sum(1 for r in results if r["status"] == "failed")
203
+ inconclusive = sum(1 for r in results if r["status"] == "inconclusive")
204
+ # functional_status: verified only if >=1 behavior passed and NONE failed;
205
+ # failed if any behavior failed; inconclusive if nothing could be exercised.
206
+ if failed > 0:
207
+ status = "failed"
208
+ elif passed > 0 and inconclusive == 0:
209
+ status = "verified"
210
+ elif passed > 0:
211
+ status = "partial"
212
+ else:
213
+ status = "inconclusive"
214
+ return {"passed": passed, "failed": failed, "inconclusive": inconclusive,
215
+ "functional_status": status}
216
+
217
+
218
+ def run(spec_text: str, base_url: str) -> dict:
219
+ assertions = derive_endpoint_assertions(spec_text)
220
+ if not assertions:
221
+ return {"spec_behaviors": [], "summary": {"passed": 0, "failed": 0,
222
+ "inconclusive": 0, "functional_status": "inconclusive"},
223
+ "note": "no HTTP endpoint behaviors derivable from the spec"}
224
+ results = verify_crud_lifecycle(base_url, assertions)
225
+ summ = summarize(results)
226
+ return {"assertions_derived": assertions, "spec_behaviors": results,
227
+ "summary": summ, "functional_status": summ["functional_status"]}
228
+
229
+
230
+ def main(argv):
231
+ if len(argv) < 3:
232
+ print("usage: functional-verify.py <spec_file> <base_url>", file=sys.stderr)
233
+ return 2
234
+ spec_file, base_url = argv[1], argv[2]
235
+ try:
236
+ with open(spec_file, errors="replace") as f:
237
+ spec_text = f.read()
238
+ except OSError as e:
239
+ print(json.dumps({"error": f"cannot read spec: {e}"}))
240
+ return 1
241
+ print(json.dumps(run(spec_text, base_url), indent=2))
242
+ return 0
243
+
244
+
245
+ if __name__ == "__main__":
246
+ sys.exit(main(sys.argv))
@@ -287,9 +287,22 @@ def _collect_quality_gates(loki_dir):
287
287
  elif os.path.exists(result_json):
288
288
  rj = _read_json(result_json, default=None)
289
289
  if isinstance(rj, dict):
290
- status = _norm_gate_status(
291
- rj.get("passed", rj.get("status", "not_run"))
292
- )
290
+ # Read the marker's outcome key. enforce_static_analysis writes
291
+ # `"pass"` (a bool); other markers may write `"passed"` or
292
+ # `"status"`. Try all three so a real result is NEVER misread as
293
+ # not_run: a failing static-analysis marker ({"pass":false,
294
+ # "findings":11}) was collapsing to not_run (the reader looked
295
+ # only for "passed"/"status"), understating a real gate FAILURE
296
+ # as "did not run" -- the receipt read "gaps" where it should
297
+ # read a failed gate. _norm_gate_status maps a bool correctly
298
+ # (True->passed, False->failed). A key that is genuinely absent
299
+ # still defaults to not_run (honest -- never fabricated passed).
300
+ if "pass" in rj:
301
+ status = _norm_gate_status(rj.get("pass"))
302
+ else:
303
+ status = _norm_gate_status(
304
+ rj.get("passed", rj.get("status", "not_run"))
305
+ )
293
306
  if status is not None:
294
307
  gates.append({"name": gate_name, "status": status})
295
308
  seen.add(gate_name)
@@ -431,6 +444,64 @@ def _norm_tests_status(raw):
431
444
  return s
432
445
 
433
446
 
447
+ def _collect_functional(loki_dir):
448
+ """Read .loki/quality/functional-results.json (the FV-1 functional harness).
449
+
450
+ Deterministic FACT: did the built app actually DO what the spec asked (run the
451
+ app + exercise spec-derived behaviors -- POST persists, GET reflects, ...), as
452
+ opposed to just compiling and passing unit tests. Tolerates an absent file ->
453
+ status not_run. Shape mirrors the FV-1 harness output:
454
+ {ran, functional_status, passed, failed, inconclusive}.
455
+
456
+ DESCRIPTIVE ONLY (FV-2, record half): this fact is RECORDED on the receipt but
457
+ is deliberately NOT read by _compute_headline / _compute_degraded, so it does
458
+ NOT change what "Verified" means. Making functional-satisfaction gate the green
459
+ headline is a trust-semantics product decision (council + founder), the second
460
+ half of FV-2. Recording it first lets the signal be seen and validated safely.
461
+ """
462
+ out = {"ran": False, "functional_status": "not_run",
463
+ "passed": 0, "failed": 0, "inconclusive": 0}
464
+ raw = _read_json(
465
+ os.path.join(loki_dir, "quality", "functional-results.json"), default=None
466
+ )
467
+ if not isinstance(raw, dict):
468
+ return out
469
+ out["ran"] = True
470
+ out["functional_status"] = str(raw.get("functional_status") or "inconclusive")
471
+ summary = raw.get("summary") if isinstance(raw.get("summary"), dict) else raw
472
+ for k in ("passed", "failed", "inconclusive"):
473
+ v = summary.get(k)
474
+ if isinstance(v, int):
475
+ out[k] = v
476
+ return out
477
+
478
+
479
+ def _collect_healthcheck(loki_dir):
480
+ """Read .loki/app-runner/health.json (the app-runner liveness probe).
481
+
482
+ Deterministic FACT: did the built app actually come up and respond (HTTP/PID
483
+ health), as written by app-runner. Absent -> not_run. Shape:
484
+ {ran, ok, status, checked_at}. status: not_run (never checked) | healthy
485
+ (ran, ok:true) | unhealthy (ran, ok:false).
486
+
487
+ DESCRIPTIVE ONLY (Evidence Receipt record half): recorded for transparency,
488
+ NOT read by _compute_headline / _compute_degraded, so it does not change what
489
+ "Verified" means. Gating on it is the founder-gated trust decision (mirrors the
490
+ FV-2 opt-in gate). ponytail: reuses the health.json app-runner already writes.
491
+ """
492
+ out = {"ran": False, "ok": False, "status": "not_run", "checked_at": ""}
493
+ raw = _read_json(
494
+ os.path.join(loki_dir, "app-runner", "health.json"), default=None
495
+ )
496
+ if not isinstance(raw, dict):
497
+ return out
498
+ out["ran"] = True
499
+ out["ok"] = bool(raw.get("ok"))
500
+ out["checked_at"] = str(raw.get("checked_at") or "")
501
+ out["status"] = "healthy" if out["ok"] else "unhealthy"
502
+ return out
503
+
504
+
434
505
  def _collect_tests(loki_dir):
435
506
  """Read .loki/quality/test-results.json.
436
507
 
@@ -763,6 +834,8 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
763
834
  build = _collect_build(loki_dir)
764
835
  tests = _collect_tests(loki_dir)
765
836
  security = _collect_security(loki_dir)
837
+ functional = _collect_functional(loki_dir) # FV-2 record-half: descriptive only
838
+ healthcheck = _collect_healthcheck(loki_dir) # Evidence Receipt record-half
766
839
  evidence_gate = _collect_evidence_gate(loki_dir)
767
840
 
768
841
  deployed_url = os.environ.get("LOKI_DEPLOYED_URL") or None
@@ -800,6 +873,14 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
800
873
  for g in (quality_gates.get("gates") or [])
801
874
  ],
802
875
  "security": security,
876
+ # FV-2 (record half): did the app actually DO what the spec asked? Present
877
+ # for transparency; DELIBERATELY NOT read by _compute_headline /
878
+ # _compute_degraded, so it does not (yet) change the verdict. Wiring it into
879
+ # the green headline is the founder-gated trust-semantics decision.
880
+ "functional": functional,
881
+ # Evidence Receipt (record half): did the built app come up + respond?
882
+ # Descriptive; NOT read by _compute_headline (gating is founder-gated).
883
+ "healthcheck": healthcheck,
803
884
  "cost": cost,
804
885
  "meta": {
805
886
  "run_id": run_id,
@@ -964,12 +1045,25 @@ def _compute_headline(facts, degraded):
964
1045
  # intent). This keeps the receipt honest about security, not just tests.
965
1046
  sec = facts.get("security") or {}
966
1047
  sec_high = bool(sec.get("ran") and (sec.get("high_active") or 0) > 0)
1048
+ # FV-2 gate (opt-in via LOKI_FV_GATE=1, default OFF -> headline unchanged). When
1049
+ # enabled, a functional check that RAN and FAILED (the built app does not do what
1050
+ # the spec asked -- a static shell for a backend spec) is a hard failure, same
1051
+ # class as a failed test. Default-off keeps every existing build's verdict
1052
+ # byte-identical; the founder flips it on after reviewing the reclassification.
1053
+ # ponytail: reads the already-recorded functional fact; no new plumbing.
1054
+ fn = facts.get("functional") or {}
1055
+ fn_failed = bool(
1056
+ os.environ.get("LOKI_FV_GATE") == "1"
1057
+ and fn.get("ran")
1058
+ and fn.get("functional_status") == "failed"
1059
+ )
967
1060
  any_failed = (
968
1061
  tests.get("status") == "failed"
969
1062
  or build.get("status") == "failed"
970
1063
  or any(g.get("status") == "failed"
971
1064
  for g in (facts.get("quality_gates") or []))
972
1065
  or sec_high
1066
+ or fn_failed
973
1067
  )
974
1068
  if any_failed:
975
1069
  return "NOT VERIFIED"
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ # scaffold-hook.sh -- opt-in seam that wires the contract-first scaffold (M1-M3)
3
+ # into the build lane. Mirrors the LOKI_SENTRUX_GATE pattern: sourced ONLY when
4
+ # the gate is enabled, and no-ops safely otherwise.
5
+ #
6
+ # ENABLED by LOKI_SCAFFOLD_CONTRACT_FIRST=1 (default OFF -> the default build path
7
+ # is byte-identical and completely unaffected). When ON, and ONLY for a GREENFIELD
8
+ # target (an effectively-empty project dir) whose PRD implies a web/API backend,
9
+ # it lays down the contract-first + real-backend + design-system skeleton BEFORE
10
+ # the SDLC loop, so the model builds ON a real wired substrate instead of
11
+ # hand-writing disconnected files. It NEVER touches a non-empty/brownfield repo.
12
+ #
13
+ # It is deliberately conservative: on any doubt it does nothing (returns 0) and
14
+ # lets the normal build proceed -- it can only ADD a starting skeleton, never
15
+ # remove or overwrite existing code.
16
+ #
17
+ # PARITY: this is a bash-only ORCHESTRATION seam in run_autonomous, NOT part of
18
+ # the byte-mirrored build_prompt() contract (build_prompt.ts is unchanged). It
19
+ # mirrors the LOKI_SENTRUX_GATE precedent, which is likewise bash-orchestration
20
+ # only (not behavior-mirrored in the bun runner). Because it is DEFAULT-OFF, the
21
+ # bun default path is unaffected. When the bun runner (loki-ts autonomous.ts,
22
+ # currently a skeleton port) reaches run_autonomous behavior parity, it should
23
+ # mirror this opt-in gate.
24
+
25
+ # Is the target dir greenfield (no meaningful source yet)? Ignores .loki, .git,
26
+ # the PRD, and dotfiles. Echoes "yes"/"no".
27
+ _scaffold_is_greenfield() {
28
+ local dir="${1:-.}"
29
+ local n
30
+ n=$(find "$dir" -type f \
31
+ -not -path "*/.loki/*" -not -path "*/.git/*" \
32
+ -not -name "PRD.md" -not -name "prd.md" -not -name ".*" \
33
+ -not -path "*/node_modules/*" 2>/dev/null | head -5 | wc -l | tr -d ' ')
34
+ [ "${n:-0}" -eq 0 ] && echo "yes" || echo "no"
35
+ }
36
+
37
+ # Derive a resource + fields from a PRD, conservatively. Only fires when the PRD
38
+ # clearly names a REST resource; otherwise echoes nothing (caller skips).
39
+ # Kept intentionally simple + general (no product-specific knowledge).
40
+ _scaffold_derive_resource() {
41
+ local prd="$1"
42
+ [ -f "$prd" ] || return 0
43
+ # Look for an explicit "GET/POST /api/<collection>" the way FV-1 does.
44
+ local coll
45
+ coll=$(grep -oiE '(GET|POST)[[:space:]]+/api/[a-z][a-z0-9_-]+' "$prd" 2>/dev/null \
46
+ | grep -oE '/api/[a-z][a-z0-9_-]+' | head -1 | sed 's#/api/##')
47
+ [ -z "$coll" ] && return 0
48
+ # singularize naively for the resource name
49
+ local res="${coll%s}"; [ -z "$res" ] && res="$coll"
50
+ echo "$res"
51
+ }
52
+
53
+ # The hook. Args: <target_dir> <prd_path>. Safe to call unconditionally; it
54
+ # self-gates on the flag + greenfield + a derivable resource.
55
+ run_contract_scaffold_hook() {
56
+ [ "${LOKI_SCAFFOLD_CONTRACT_FIRST:-0}" = "1" ] || return 0
57
+ local dir="${1:-.}" prd="${2:-}"
58
+ local script_dir
59
+ script_dir="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
60
+
61
+ [ "$(_scaffold_is_greenfield "$dir")" = "yes" ] || {
62
+ log_info "contract-scaffold: target is not greenfield -> skipping (never overwrites existing code)" 2>/dev/null || true
63
+ return 0
64
+ }
65
+ local res
66
+ res=$(_scaffold_derive_resource "$prd")
67
+ [ -z "$res" ] && {
68
+ log_info "contract-scaffold: no REST resource derivable from the PRD -> skipping" 2>/dev/null || true
69
+ return 0
70
+ }
71
+
72
+ local scaffold="${script_dir}/lib/contract-scaffold/scaffold.sh"
73
+ local generate="${script_dir}/lib/contract-scaffold/generate.mjs"
74
+ [ -f "$scaffold" ] && [ -f "$generate" ] || return 0
75
+
76
+ log_info "contract-scaffold: greenfield + resource '$res' -> laying down contract-first + backend + design skeleton" 2>/dev/null || true
77
+ bash "$scaffold" "$dir" "$res" 2>/dev/null || true
78
+ if command -v node >/dev/null 2>&1; then
79
+ node "$generate" "$dir" "$res" 2>/dev/null || true
80
+ fi
81
+ return 0
82
+ }
package/autonomy/run.sh CHANGED
@@ -8511,6 +8511,30 @@ enforce_build_check() {
8511
8511
  return 0
8512
8512
  }
8513
8513
 
8514
+ # True when the workspace's package.json declares a non-empty `lint` script, so
8515
+ # the static-analysis gate can run the app's OWN linter (oxlint/eslint/biome/...)
8516
+ # via `npm run lint` rather than only recognizing eslint config files. Pure read
8517
+ # of package.json; returns non-zero on absence / no lint script / unreadable.
8518
+ has_npm_lint_script() {
8519
+ local dir="${1:-.}"
8520
+ local pkg="$dir/package.json"
8521
+ [ -f "$pkg" ] || return 1
8522
+ # Parse with python3 (already a hard dep of the engine) so a `"lint":` inside
8523
+ # a string value or comment cannot yield a false positive; require a real,
8524
+ # non-empty scripts.lint entry.
8525
+ python3 - "$pkg" <<'PYEOF' 2>/dev/null
8526
+ import json, sys
8527
+ try:
8528
+ with open(sys.argv[1]) as fh:
8529
+ data = json.load(fh)
8530
+ except Exception:
8531
+ sys.exit(1)
8532
+ scripts = data.get("scripts") or {}
8533
+ lint = scripts.get("lint")
8534
+ sys.exit(0 if isinstance(lint, str) and lint.strip() else 1)
8535
+ PYEOF
8536
+ }
8537
+
8514
8538
  enforce_static_analysis() {
8515
8539
  local loki_dir="${TARGET_DIR:-.}/.loki"
8516
8540
  local quality_dir="$loki_dir/quality"
@@ -8539,6 +8563,31 @@ enforce_static_analysis() {
8539
8563
  done
8540
8564
  if [ -n "$abs_files" ]; then
8541
8565
  total_checked=$((total_checked + $(echo "$abs_files" | wc -w)))
8566
+ # v7.x (static-analysis honest coverage): ADDITIVELY run the app's OWN
8567
+ # declared `lint` script when present. Generated apps increasingly use
8568
+ # oxlint / biome (not eslint) -- e.g. package.json `"lint": "oxlint"`
8569
+ # with a `.oxlintrc.json` -- which the eslint-config probe below does
8570
+ # not recognize, so the app's real lint check was skipped. Running
8571
+ # `npm run lint` respects whatever linter the app declares. This is
8572
+ # ADDITIVE (not a replacement): we STILL run the eslint/tsc type checks
8573
+ # below, so a lint pass never silences the type-error check for TS apps
8574
+ # (oxlint is not type-aware). rc 127 = the declared linter is not
8575
+ # installed/resolvable -> HONEST skip, never counted as a violation
8576
+ # (rc-only signal; a real lint failure whose OUTPUT mentions "not
8577
+ # found" must still count, so we do NOT grep the message).
8578
+ if has_npm_lint_script "${TARGET_DIR:-.}"; then
8579
+ local lint_out lint_rc=0
8580
+ lint_out=$(cd "${TARGET_DIR:-.}" && npm run --silent lint 2>&1) || lint_rc=$?
8581
+ if [ "$lint_rc" -eq 127 ]; then
8582
+ log_info "Static analysis: app 'lint' script did not resolve a linter (not run, honest skip)"
8583
+ elif [ "$lint_rc" -ne 0 ]; then
8584
+ findings=$((findings + 1))
8585
+ details="${details}Lint (npm run lint): $(echo "$lint_out" | tail -3 | tr '\n' ' '). "
8586
+ fi
8587
+ fi
8588
+ # Type/syntax check path (unchanged contract): eslint when configured,
8589
+ # else tsc project-mode + per-file parse. Runs REGARDLESS of the lint
8590
+ # step above so TS type errors are always caught.
8542
8591
  if [ -f "${TARGET_DIR:-.}/.eslintrc.js" ] || [ -f "${TARGET_DIR:-.}/.eslintrc.json" ] || \
8543
8592
  [ -f "${TARGET_DIR:-.}/eslint.config.js" ] || [ -f "${TARGET_DIR:-.}/eslint.config.mjs" ]; then
8544
8593
  local eslint_out
@@ -8559,7 +8608,7 @@ enforce_static_analysis() {
8559
8608
  if [ -f "${TARGET_DIR:-.}/tsconfig.json" ] && command -v tsc &>/dev/null; then
8560
8609
  local _has_ts=0
8561
8610
  for f in $abs_files; do
8562
- case "$f" in *.ts|*.tsx) _has_ts=1; break ;; esac
8611
+ case "$f" in *.ts|*.tsx|*.jsx) _has_ts=1; break ;; esac
8563
8612
  done
8564
8613
  if [ "$_has_ts" -eq 1 ]; then
8565
8614
  _ts_project_mode=1
@@ -8569,7 +8618,7 @@ enforce_static_analysis() {
8569
8618
  local _changed_ts_errors=""
8570
8619
  for f in $js_files; do
8571
8620
  case "$f" in
8572
- *.ts|*.tsx)
8621
+ *.ts|*.tsx|*.jsx)
8573
8622
  # tsc emits paths relative to project root with `(line,col):` suffix.
8574
8623
  # v7.5.12 Dev11 (R1 MED): use grep -F (literal) so filenames
8575
8624
  # containing regex metacharacters cannot cause false positives
@@ -8591,11 +8640,16 @@ enforce_static_analysis() {
8591
8640
  fi
8592
8641
  fi
8593
8642
  for f in $abs_files; do
8594
- # node --check cannot parse TypeScript / TSX files; it
8595
- # crashes with ERR_UNKNOWN_FILE_EXTENSION. Skip them when
8596
- # tsc is not available; otherwise delegate to tsc.
8643
+ # node --check cannot parse TypeScript / TSX / JSX files; it
8644
+ # crashes with ERR_UNKNOWN_FILE_EXTENSION (JSX) or a parse error
8645
+ # (TS syntax). Route .ts/.tsx AND .jsx to the JSX-capable tsc
8646
+ # path; only plain .js/.mjs/.cjs go through node --check. Before
8647
+ # the .jsx addition here, every valid .jsx file was falsely
8648
+ # reported as a "Syntax error" (node --check ERR_UNKNOWN_FILE_
8649
+ # EXTENSION) -- a whole class of false static-analysis findings
8650
+ # on React apps.
8597
8651
  case "$f" in
8598
- *.ts|*.tsx)
8652
+ *.ts|*.tsx|*.jsx)
8599
8653
  # When tsconfig project-mode handled it above, skip
8600
8654
  # the per-file fallback to avoid duplicate / false errors.
8601
8655
  if [ "$_ts_project_mode" -eq 1 ]; then
@@ -16254,6 +16308,21 @@ run_autonomous() {
16254
16308
  esac
16255
16309
  fi
16256
16310
 
16311
+ # Contract-first scaffold seam (opt-in via LOKI_SCAFFOLD_CONTRACT_FIRST=1,
16312
+ # default OFF -> default build path is byte-identical). Mirrors the
16313
+ # LOKI_SENTRUX_GATE pattern: source the helper only when the gate is enabled,
16314
+ # and it self-gates further on greenfield + a derivable REST resource. It can
16315
+ # ONLY add a starting skeleton (contract-first codegen + real backend + design
16316
+ # system, M1-M3); it never overwrites existing code or touches a brownfield
16317
+ # repo. On any doubt it no-ops and the normal build proceeds unchanged.
16318
+ if [ "${LOKI_SCAFFOLD_CONTRACT_FIRST:-0}" = "1" ]; then
16319
+ # shellcheck disable=SC1090,SC1091
16320
+ source "${SCRIPT_DIR}/lib/scaffold-hook.sh" 2>/dev/null || true
16321
+ if type run_contract_scaffold_hook >/dev/null 2>&1; then
16322
+ run_contract_scaffold_hook "${TARGET_DIR:-.}" "$prd_path" || true
16323
+ fi
16324
+ fi
16325
+
16257
16326
  # Auto-detect PRD if not provided
16258
16327
  if [ -z "$prd_path" ]; then
16259
16328
  log_step "No PRD provided, searching for existing PRD files..."
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.121.3"
10
+ __version__ = "7.121.4"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.121.3
5
+ **Version:** v7.121.4
6
6
 
7
7
  ---
8
8
 
@@ -396,7 +396,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
396
396
  # Run Loki Mode in Docker (Claude provider, API-key auth)
397
397
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
398
398
  -v $(pwd):/workspace -w /workspace \
399
- asklokesh/loki-mode:7.121.3 start ./my-spec.md
399
+ asklokesh/loki-mode:7.121.4 start ./my-spec.md
400
400
  ```
401
401
 
402
402
  ##### docker compose + .env (no host install)
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.121.3";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=B8(M8(import.meta.url)),Z=$1(Q);Q$=U8(Y8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var Q1=P(()=>{C()});var p1={};b(p1,{runOrThrow:()=>S8,run:()=>R,readStreamCapped:()=>c1,commandVersion:()=>C8,commandExists:()=>u,ShellError:()=>Z1,MAX_STDOUT_BYTES:()=>u1});async function c1($,Q=u1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function R($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([c1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function S8($,Q={}){let Z=await R($,Q);if(Z.exitCode!==0)throw new Z1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function u($){let Q=D8($),Z=await R(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function D8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function C8($,Q="--version"){if(!await u($))return null;let z=await R([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var u1=16777216,Z1;var o=P(()=>{Z1=class Z1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function r($){return b8?"":$}var b8,M,S,_,tZ,L,k,y,J;var p=P(()=>{b8=(process.env.NO_COLOR??"").length>0;M=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),tZ=r("\x1B[0;34m"),L=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as l8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(l8($))return A$=$,$;let Q=await u("python3.12");if(Q)return A$=Q,Q;let Z=await u("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return R([Z,"-c",$],Q)}var A$;var V$=P(()=>{o()});var V0={};b(V0,{runStatus:()=>U3});import{existsSync as v,readFileSync as H$,readdirSync as $0,statSync as Q0}from"fs";import{resolve as D,basename as z3}from"path";import{homedir as X3}from"os";function Z0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*C$/Q);if(X>C$)X=C$;let q=C$-X,K=S;if(z>=80)K=M;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Z0($),H=Z0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${H})`}async function q3(){if(await u("jq"))return!0;return process.stdout.write(`${M}Error: jq is required but not installed.${J}
2
+ var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.121.4";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=B8(M8(import.meta.url)),Z=$1(Q);Q$=U8(Y8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var Q1=P(()=>{C()});var p1={};b(p1,{runOrThrow:()=>S8,run:()=>R,readStreamCapped:()=>c1,commandVersion:()=>C8,commandExists:()=>u,ShellError:()=>Z1,MAX_STDOUT_BYTES:()=>u1});async function c1($,Q=u1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function R($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([c1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function S8($,Q={}){let Z=await R($,Q);if(Z.exitCode!==0)throw new Z1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function u($){let Q=D8($),Z=await R(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function D8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function C8($,Q="--version"){if(!await u($))return null;let z=await R([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var u1=16777216,Z1;var o=P(()=>{Z1=class Z1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function r($){return b8?"":$}var b8,M,S,_,tZ,L,k,y,J;var p=P(()=>{b8=(process.env.NO_COLOR??"").length>0;M=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),tZ=r("\x1B[0;34m"),L=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as l8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(l8($))return A$=$,$;let Q=await u("python3.12");if(Q)return A$=Q,Q;let Z=await u("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return R([Z,"-c",$],Q)}var A$;var V$=P(()=>{o()});var V0={};b(V0,{runStatus:()=>U3});import{existsSync as v,readFileSync as H$,readdirSync as $0,statSync as Q0}from"fs";import{resolve as D,basename as z3}from"path";import{homedir as X3}from"os";function Z0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*C$/Q);if(X>C$)X=C$;let q=C$-X,K=S;if(z>=80)K=M;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Z0($),H=Z0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${H})`}async function q3(){if(await u("jq"))return!0;return process.stdout.write(`${M}Error: jq is required but not installed.${J}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -814,4 +814,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
814
814
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
815
815
  `),process.stderr.write(K8),2}}e1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var SZ=await NZ(Bun.argv.slice(2));process.exit(SZ);
816
816
 
817
- //# debugId=B98C998AFA90A20E64756E2164756E21
817
+ //# debugId=8F986B79CB65C96964756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.121.3'
60
+ __version__ = '7.121.4'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.121.3",
4
+ "version": "7.121.4",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "7.121.3",
5
+ "version": "7.121.4",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",