@appchy/jarvis 0.1.37 → 0.1.39
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/bin/config-change.mjs +1 -1
- package/bin/jarvis.mjs +8 -12
- package/bin/pre-tool-use.mjs +2 -2
- package/bin/session-start.mjs +1 -1
- package/bin/stop.mjs +1 -1
- package/bin/user-prompt-submit.mjs +1 -1
- package/dist/bin.js +10168 -37341
- package/dist/bin.js.map +1 -1
- package/dist/data/backends.mjs +469 -0
- package/dist/data/chunk-7REP35VA.mjs +462 -0
- package/dist/data/chunk-AKQQC5IT.mjs +1224 -0
- package/dist/data/chunk-AYOJSS2F.mjs +72 -0
- package/dist/data/chunk-RRJ6KKYL.mjs +42 -0
- package/dist/data/chunk-YWSWQEJN.mjs +138 -0
- package/dist/data/embedders.mjs +80 -0
- package/dist/data/finders.mjs +35 -0
- package/dist/data/index.mjs +137 -0
- package/dist/data/linkers.mjs +83 -0
- package/dist/data/mcp.mjs +2624 -0
- package/dist/data/persistences.mjs +8 -0
- package/dist/data/rerankers.mjs +52 -0
- package/dist/data/stores.mjs +38 -0
- package/dist/hooks/config-change.js +7 -29
- package/dist/hooks/config-change.js.map +1 -1
- package/dist/hooks/pre-tool-use.js +129 -32
- package/dist/hooks/pre-tool-use.js.map +1 -1
- package/dist/hooks/session-start.js +72 -37
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop.js +140 -32
- package/dist/hooks/stop.js.map +1 -1
- package/dist/hooks/user-prompt-submit.js +7 -29
- package/dist/hooks/user-prompt-submit.js.map +1 -1
- package/harness/assets/architecture-template.md +53 -0
- package/harness/assets/design-template.md +89 -0
- package/harness/assets/domains/architecture.md +43 -0
- package/harness/assets/domains/commercial.md +42 -0
- package/harness/assets/domains/design.md +42 -0
- package/harness/assets/domains/legal.md +43 -0
- package/harness/assets/domains/operations.md +39 -0
- package/harness/assets/domains/product.md +43 -0
- package/harness/assets/domains/quality.md +43 -0
- package/harness/assets/domains/security.md +44 -0
- package/harness/assets/domains/support.md +45 -0
- package/harness/assets/epic-template.md +85 -0
- package/harness/assets/feature-template.md +86 -0
- package/harness/assets/handoff-template.md +26 -0
- package/harness/assets/task-template.md +38 -0
- package/harness/assets/version-template.md +43 -0
- package/harness/harness/__init__.py +7 -0
- package/harness/harness/align.py +582 -0
- package/harness/harness/architecture.py +254 -0
- package/harness/harness/autonomy.py +374 -0
- package/harness/harness/branches.py +408 -0
- package/harness/harness/config.py +1482 -0
- package/harness/harness/coverage.py +199 -0
- package/harness/harness/epic.py +220 -0
- package/harness/harness/events.py +153 -0
- package/harness/harness/extend.py +99 -0
- package/harness/harness/frontmatter.py +218 -0
- package/harness/harness/gate.py +591 -0
- package/harness/harness/generate.py +267 -0
- package/harness/harness/git.py +775 -0
- package/harness/harness/ids.py +140 -0
- package/harness/harness/kickoff.py +231 -0
- package/harness/harness/lint.py +505 -0
- package/harness/harness/model.py +364 -0
- package/harness/harness/peers.py +187 -0
- package/harness/harness/product.py +29 -0
- package/harness/harness/registry.py +382 -0
- package/harness/harness/report.py +227 -0
- package/harness/harness/safety.py +387 -0
- package/harness/harness/scaffold.py +129 -0
- package/harness/harness/shard.py +63 -0
- package/harness/harness/shift.py +348 -0
- package/harness/harness/task.py +507 -0
- package/harness/harness/tree.py +258 -0
- package/harness/harness/version.py +305 -0
- package/harness/harness/wrap.py +217 -0
- package/harness/hooks/guard.py +259 -0
- package/harness/presets/appchy/PRESET.md +717 -0
- package/harness/presets/appchy/references/artifacts.md +539 -0
- package/harness/presets/appchy/references/graph.md +154 -0
- package/harness/presets/appchy/references/operations.md +444 -0
- package/harness/presets/appchy/references/research.md +216 -0
- package/harness/schema/work.config.schema.json +401 -0
- package/harness/test_work.py +5002 -0
- package/harness/work.py +534 -0
- package/package.json +34 -40
- package/bin/config-change.dev.mjs +0 -17
- package/bin/jarvis-dev.mjs +0 -30
- package/bin/pre-tool-use.dev.mjs +0 -23
- package/bin/session-start.dev.mjs +0 -17
- package/bin/stop.dev.mjs +0 -17
- package/bin/user-prompt-submit.dev.mjs +0 -17
- package/dev/bin.js +0 -38821
- package/dev/bin.js.map +0 -1
- package/dev/env.json +0 -1
- package/dev/hooks/config-change.js +0 -110
- package/dev/hooks/config-change.js.map +0 -1
- package/dev/hooks/pre-tool-use.js +0 -120
- package/dev/hooks/pre-tool-use.js.map +0 -1
- package/dev/hooks/session-start.js +0 -115
- package/dev/hooks/session-start.js.map +0 -1
- package/dev/hooks/stop.js +0 -112
- package/dev/hooks/stop.js.map +0 -1
- package/dev/hooks/user-prompt-submit.js +0 -111
- package/dev/hooks/user-prompt-submit.js.map +0 -1
- package/dist/env.json +0 -1
|
@@ -5,33 +5,11 @@ import { appendFile, mkdir } from "fs/promises";
|
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
|
|
7
7
|
// src/config.ts
|
|
8
|
-
import fs from "fs";
|
|
9
8
|
import path from "path";
|
|
10
9
|
import os from "os";
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
12
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
var __dirname = path.dirname(__filename);
|
|
14
|
-
function isDev() {
|
|
15
|
-
const candidates = [
|
|
16
|
-
path.join(__dirname, "env.json"),
|
|
17
|
-
path.join(__dirname, "..", "env.json")
|
|
18
|
-
];
|
|
19
|
-
for (const envFile of candidates) {
|
|
20
|
-
try {
|
|
21
|
-
const data = JSON.parse(fs.readFileSync(envFile, "utf-8"));
|
|
22
|
-
return data.env === "development";
|
|
23
|
-
} catch {
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
function resolveConfigDir() {
|
|
29
|
-
if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;
|
|
30
|
-
const base = path.join(os.homedir(), ".jarvis");
|
|
31
|
-
return isDev() ? path.join(base, "dev") : base;
|
|
32
|
-
}
|
|
33
10
|
function configDir() {
|
|
34
|
-
|
|
11
|
+
if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;
|
|
12
|
+
return path.join(os.homedir(), ".jarvis");
|
|
35
13
|
}
|
|
36
14
|
function getHooksDir() {
|
|
37
15
|
return path.join(configDir(), "hooks");
|
|
@@ -39,9 +17,9 @@ function getHooksDir() {
|
|
|
39
17
|
|
|
40
18
|
// src/hooks/drop.ts
|
|
41
19
|
async function appendDrop(req) {
|
|
42
|
-
if (!req.sessionId || !req.
|
|
20
|
+
if (!req.sessionId || !req.type || !req.uniqId) {
|
|
43
21
|
process.stderr.write(
|
|
44
|
-
`[hook] skip ${req.
|
|
22
|
+
`[hook] skip ${req.type ?? "?"} missing field sessionId=${!!req.sessionId} uniqId=${!!req.uniqId}
|
|
45
23
|
`
|
|
46
24
|
);
|
|
47
25
|
return;
|
|
@@ -52,7 +30,7 @@ async function appendDrop(req) {
|
|
|
52
30
|
`);
|
|
53
31
|
});
|
|
54
32
|
const envelope = {
|
|
55
|
-
|
|
33
|
+
type: req.type,
|
|
56
34
|
sessionId: req.sessionId,
|
|
57
35
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
58
36
|
uniqId: req.uniqId,
|
|
@@ -62,7 +40,7 @@ async function appendDrop(req) {
|
|
|
62
40
|
try {
|
|
63
41
|
await appendFile(file, JSON.stringify(envelope) + "\n");
|
|
64
42
|
process.stderr.write(
|
|
65
|
-
`[hook] ${req.
|
|
43
|
+
`[hook] ${req.type} appended sessionId=${req.sessionId} uniqId=${req.uniqId}
|
|
66
44
|
`
|
|
67
45
|
);
|
|
68
46
|
} catch (err) {
|
|
@@ -101,7 +79,7 @@ async function main() {
|
|
|
101
79
|
...p.cwd ? { cwd: p.cwd } : {}
|
|
102
80
|
};
|
|
103
81
|
await appendDrop({
|
|
104
|
-
|
|
82
|
+
type: "user-prompt-submit",
|
|
105
83
|
sessionId,
|
|
106
84
|
uniqId: `user-prompt-submit-${ts}`,
|
|
107
85
|
data
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/drop.ts","../../src/config.ts","../../src/hooks/user-prompt-submit.ts"],"sourcesContent":["/**\n * Shared helpers for CC hook scripts.\n *\n * Each hook script (pre-tool-use, session-start, stop, …) is a tiny\n * stdin → append-line → exit binary. They all write to the same per-\n * session JSONL at `<hooksDir>/<sessionId>.jsonl`. This module\n * concentrates the envelope shape and the atomic append so the per-\n * hook scripts stay <30 lines each.\n *\n * Hot-path constraints: NEVER block, NEVER network, NEVER read user\n * input. Pure stdin parse + filesystem append + exit. Worst-case one\n * `mkdir` + one `appendFile` per fire.\n */\n\nimport { appendFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { getHooksDir } from \"../config\";\n\n/** Wire-format envelope written one-per-line to the hook JSONL.\n * The watcher in `sessions.ts` parses this exact shape — keep in sync. */\nexport interface HookDropEnvelope<TData = unknown> {\n kind: string;\n sessionId: string;\n ts: string;\n /** Idempotency key. Lets the watcher's replay-on-restart skip drops\n * whose effects were already emitted (see `HookCursor.applied`). */\n uniqId: string;\n data: TData;\n}\n\nexport interface AppendDropRequest<TData> {\n kind: string;\n sessionId: string;\n uniqId: string;\n data: TData;\n}\n\n/** Append one envelope line to the per-session hook JSONL. Atomic between\n * hook processes via POSIX `O_APPEND` (Node's `fs.appendFile`) — concurrent\n * appends from different CC sessions / hook kinds can't interleave bytes\n * within a line as long as the line stays < `PIPE_BUF` (4KB). Drops are\n * <1KB in practice, so this is safe without locks.\n *\n * Stderr logs are written so `tail -f ~/.jarvis[/dev]/hooks/hook.log` (when\n * CC pipes hook stderr there, which it does by default) shows what fired.\n * Stderr never breaks CC — it only watches stdout for decision JSON. */\nexport async function appendDrop<TData>(req: AppendDropRequest<TData>): Promise<void> {\n if (!req.sessionId || !req.kind || !req.uniqId) {\n process.stderr.write(\n `[hook] skip ${req.kind ?? \"?\"} missing field ` +\n `sessionId=${!!req.sessionId} uniqId=${!!req.uniqId}\\n`,\n );\n return;\n }\n const dir = getHooksDir();\n await mkdir(dir, { recursive: true }).catch((err) => {\n process.stderr.write(`[hook] mkdir failed ${dir}: ${err}\\n`);\n });\n const envelope: HookDropEnvelope<TData> = {\n kind: req.kind,\n sessionId: req.sessionId,\n ts: new Date().toISOString(),\n uniqId: req.uniqId,\n data: req.data,\n };\n const file = join(dir, `${req.sessionId}.jsonl`);\n try {\n await appendFile(file, JSON.stringify(envelope) + \"\\n\");\n process.stderr.write(\n `[hook] ${req.kind} appended sessionId=${req.sessionId} uniqId=${req.uniqId}\\n`,\n );\n } catch (err) {\n process.stderr.write(`[hook] append failed ${file}: ${err}\\n`);\n }\n}\n\n/** Read CC's hook payload from stdin. Returns `null` on parse failure\n * (never throws — the hook must never block CC). */\nexport async function readHookStdin<T = Record<string, unknown>>(): Promise<T | null> {\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n }\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n if (!raw) return null;\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/** Hard cap any hook so an exotic stdin stall can't pile up forever\n * before CC's terminal prompt fires. We exit 0 with no output → CC\n * proceeds with its normal permission flow. */\nexport function installHookTimeout(ms = 750): void {\n const timer = setTimeout(() => process.exit(0), ms);\n timer.unref();\n}\n","/**\n * Agent Config\n *\n * Manages ~/.jarvis/config.json — saved by `jarvis connect <token>`,\n * read on `jarvis start`.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport os from \"os\";\nimport { fileURLToPath } from \"url\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface AgentConfig {\n /** Cloud WS API URL (from connect token) */\n apiUrl?: string;\n /** JWT auth token (from connect token) */\n token?: string;\n /** Long-lived refresh token for obtaining new access tokens */\n refreshToken?: string;\n /** User ID */\n userId: string;\n /** Environment ID */\n envId?: string;\n /** Workspace root path for repo operations */\n workspacePath?: string;\n /** Web app URL for browser-based auth */\n appUrl?: string;\n /** Anthropic API key (for local-only use without cloud) */\n anthropicApiKey?: string;\n /** Use Anthropic subscription instead of API key */\n useSubscription?: boolean;\n /** When the config was last updated */\n connectedAt?: string;\n}\n\nexport interface ConnectToken {\n apiUrl: string;\n jwt: string;\n refreshToken: string;\n userId: string;\n envId: string;\n}\n\n// =============================================================================\n// Paths\n// =============================================================================\n\n// =============================================================================\n// Environment (single source of truth)\n// =============================================================================\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n/** Single source of truth for dev vs prod.\n * Two locations, picked by which file is shipped:\n * Dev: apps/cli/env.json (committed, `{ \"env\": \"development\" }`)\n * Prod: apps/cli/dist/env.json (written by tsup build, `{ \"env\": \"production\" }`)\n * We try the prod-style sibling first (next to bin.js when bundled), then fall\n * back to the dev-style parent path (one up from src/config.ts). The prod bundle\n * has no `..` parent env.json, and the dev source tree has no sibling env.json,\n * so the two never collide. Safe default if neither exists: prod. */\nexport function isDev(): boolean {\n const candidates = [\n path.join(__dirname, \"env.json\"),\n path.join(__dirname, \"..\", \"env.json\"),\n ];\n for (const envFile of candidates) {\n try {\n const data = JSON.parse(fs.readFileSync(envFile, \"utf-8\"));\n return data.env === \"development\";\n } catch {\n /* try next candidate */\n }\n }\n return false;\n}\n\nconst PROD_APP_URL = \"https://jarvis.appchy.com\";\nconst DEV_APP_URL = \"http://localhost:3000\";\n\nconst PROD_WS_URL = \"wss://jarvis.appchy.com/ws\";\nconst DEV_WS_URL = \"ws://localhost:7863\";\n\n/** App URL for the current environment. */\nexport function getDefaultAppUrl(): string {\n return isDev() ? (process.env.APP_URL || DEV_APP_URL) : PROD_APP_URL;\n}\n\n/** WS hub URL for the current environment. Hardcoded per build so the prod\n * binary can never subscribe to the dev hub (or vice versa) regardless of\n * what's stored in `config.apiUrl`. Dev permits a `JARVIS_UPSTREAM_URL`\n * override for tests/CI on a non-default port; prod has no escape hatch. */\nexport function getDefaultWsUrl(): string {\n return isDev() ? (process.env.JARVIS_UPSTREAM_URL || DEV_WS_URL) : PROD_WS_URL;\n}\n\n// =============================================================================\n// Config directory\n// =============================================================================\n\n/** Determine config directory.\n * - JARVIS_CONFIG_DIR env var takes priority (escape hatch for CI/testing)\n * - dev → ~/.jarvis/dev/\n * - prod → ~/.jarvis/\n *\n * Called fresh on every read so test environments that set\n * `JARVIS_CONFIG_DIR` AFTER module load still see the override — caching\n * the value at module init would lock tests onto the host's real\n * `~/.jarvis/dev/` and leak production state into the in-memory infra. */\nfunction resolveConfigDir(): string {\n if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;\n const base = path.join(os.homedir(), \".jarvis\");\n return isDev() ? path.join(base, \"dev\") : base;\n}\n\nfunction configDir(): string {\n return resolveConfigDir();\n}\n\nfunction configFile(): string {\n return path.join(configDir(), \"config.json\");\n}\n\n/** Absolute path to the per-daemon config directory.\n * `prod` → `~/.jarvis/`, `dev` → `~/.jarvis/dev/`. */\nexport function getConfigDir(): string {\n return configDir();\n}\n\n/** Per-daemon directory for PreToolUse hook drop files. The CC hook script\n * writes `<sessionId>/<toolUseId>.json` here; the sessions watcher consumes\n * them. Prod and dev daemons each own their own subtree so a hook installed\n * for one never feeds the other. */\nexport function getHooksDir(): string {\n return path.join(configDir(), \"hooks\");\n}\n\n// =============================================================================\n// Operations\n// =============================================================================\n\nexport function loadConfig(): AgentConfig | null {\n try {\n const raw = fs.readFileSync(configFile(), \"utf-8\");\n return JSON.parse(raw) as AgentConfig;\n } catch {\n return null;\n }\n}\n\nexport function saveConfig(config: AgentConfig): void {\n fs.mkdirSync(configDir(), { recursive: true });\n fs.writeFileSync(configFile(), JSON.stringify(config, null, 2) + \"\\n\");\n}\n\nexport function clearConfig(): void {\n try {\n fs.unlinkSync(configFile());\n } catch {}\n}\n\nexport function parseConnectToken(token: string): ConnectToken {\n try {\n const decoded = Buffer.from(token, \"base64\").toString(\"utf-8\");\n const parsed = JSON.parse(decoded);\n if (!parsed.apiUrl || !parsed.jwt || !parsed.userId) {\n throw new Error(\"Invalid token: missing required fields (apiUrl, jwt, userId)\");\n }\n return parsed as ConnectToken;\n } catch (err) {\n if (err instanceof SyntaxError) {\n throw new Error(\"Invalid token: not valid base64-encoded JSON\");\n }\n throw err;\n }\n}\n\nexport function getConfigPath(): string {\n return configFile();\n}\n","/**\n * UserPromptSubmit hook — drops an envelope each time a terminal `claude`\n * user submits a prompt. The daemon SDK runs with `settingSources: []`\n * so its own `query()` calls never trigger this hook — every drop\n * observed here is unambiguously a terminal-typed turn.\n *\n * Used to flip `SessionCacheEntry.controller` to `\"claudeCode\"` on the\n * watcher side, surfacing the controller change in the web UI's pill.\n */\n\nimport { appendDrop, installHookTimeout, readHookStdin } from \"./drop\";\n\ninterface CcUserPromptSubmitInput {\n session_id?: string;\n transcript_path?: string;\n cwd?: string;\n prompt?: string;\n}\n\ninterface UserPromptSubmitDropData {\n transcriptPath?: string;\n cwd?: string;\n}\n\ninstallHookTimeout();\n\nasync function main(): Promise<void> {\n const p = await readHookStdin<CcUserPromptSubmitInput>();\n if (!p) return;\n const sessionId = p.session_id;\n if (!sessionId) return;\n\n const ts = new Date().toISOString();\n const data: UserPromptSubmitDropData = {\n ...(p.transcript_path ? { transcriptPath: p.transcript_path } : {}),\n ...(p.cwd ? { cwd: p.cwd } : {}),\n };\n\n await appendDrop<UserPromptSubmitDropData>({\n kind: \"user-prompt-submit\",\n sessionId,\n uniqId: `user-prompt-submit-${ts}`,\n data,\n });\n}\n\nvoid main().finally(() => process.exit(0));\n"],"mappings":";;;AAcA,SAAS,YAAY,aAAa;AAClC,SAAS,YAAY;;;ACRrB,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AA6C9B,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,KAAK,QAAQ,UAAU;AAUlC,SAAS,QAAiB;AAC/B,QAAM,aAAa;AAAA,IACjB,KAAK,KAAK,WAAW,UAAU;AAAA,IAC/B,KAAK,KAAK,WAAW,MAAM,UAAU;AAAA,EACvC;AACA,aAAW,WAAW,YAAY;AAChC,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,GAAG,aAAa,SAAS,OAAO,CAAC;AACzD,aAAO,KAAK,QAAQ;AAAA,IACtB,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAkCA,SAAS,mBAA2B;AAClC,MAAI,QAAQ,IAAI,kBAAmB,QAAO,QAAQ,IAAI;AACtD,QAAM,OAAO,KAAK,KAAK,GAAG,QAAQ,GAAG,SAAS;AAC9C,SAAO,MAAM,IAAI,KAAK,KAAK,MAAM,KAAK,IAAI;AAC5C;AAEA,SAAS,YAAoB;AAC3B,SAAO,iBAAiB;AAC1B;AAgBO,SAAS,cAAsB;AACpC,SAAO,KAAK,KAAK,UAAU,GAAG,OAAO;AACvC;;;AD7FA,eAAsB,WAAkB,KAA8C;AACpF,MAAI,CAAC,IAAI,aAAa,CAAC,IAAI,QAAQ,CAAC,IAAI,QAAQ;AAC9C,YAAQ,OAAO;AAAA,MACb,eAAe,IAAI,QAAQ,GAAG,4BACf,CAAC,CAAC,IAAI,SAAS,WAAW,CAAC,CAAC,IAAI,MAAM;AAAA;AAAA,IACvD;AACA;AAAA,EACF;AACA,QAAM,MAAM,YAAY;AACxB,QAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC,EAAE,MAAM,CAAC,QAAQ;AACnD,YAAQ,OAAO,MAAM,uBAAuB,GAAG,KAAK,GAAG;AAAA,CAAI;AAAA,EAC7D,CAAC;AACD,QAAM,WAAoC;AAAA,IACxC,MAAM,IAAI;AAAA,IACV,WAAW,IAAI;AAAA,IACf,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC3B,QAAQ,IAAI;AAAA,IACZ,MAAM,IAAI;AAAA,EACZ;AACA,QAAM,OAAO,KAAK,KAAK,GAAG,IAAI,SAAS,QAAQ;AAC/C,MAAI;AACF,UAAM,WAAW,MAAM,KAAK,UAAU,QAAQ,IAAI,IAAI;AACtD,YAAQ,OAAO;AAAA,MACb,UAAU,IAAI,IAAI,uBAAuB,IAAI,SAAS,WAAW,IAAI,MAAM;AAAA;AAAA,IAC7E;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,OAAO,MAAM,wBAAwB,IAAI,KAAK,GAAG;AAAA,CAAI;AAAA,EAC/D;AACF;AAIA,eAAsB,gBAAgE;AACpF,MAAI;AACF,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,OAAO,UAAU,WAAW,OAAO,KAAK,KAAK,IAAI,KAAK;AAAA,IACpE;AACA,UAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO;AAClD,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,mBAAmB,KAAK,KAAW;AACjD,QAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK,CAAC,GAAG,EAAE;AAClD,QAAM,MAAM;AACd;;;AE3EA,mBAAmB;AAEnB,eAAe,OAAsB;AACnC,QAAM,IAAI,MAAM,cAAuC;AACvD,MAAI,CAAC,EAAG;AACR,QAAM,YAAY,EAAE;AACpB,MAAI,CAAC,UAAW;AAEhB,QAAM,MAAK,oBAAI,KAAK,GAAE,YAAY;AAClC,QAAM,OAAiC;AAAA,IACrC,GAAI,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,gBAAgB,IAAI,CAAC;AAAA,IACjE,GAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC;AAAA,EAChC;AAEA,QAAM,WAAqC;AAAA,IACzC,MAAM;AAAA,IACN;AAAA,IACA,QAAQ,sBAAsB,EAAE;AAAA,IAChC;AAAA,EACF,CAAC;AACH;AAEA,KAAK,KAAK,EAAE,QAAQ,MAAM,QAAQ,KAAK,CAAC,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/drop.ts","../../src/config.ts","../../src/hooks/user-prompt-submit.ts"],"sourcesContent":["/**\n * Shared helpers for CC hook scripts.\n *\n * Each hook script (pre-tool-use, session-start, stop, …) is a tiny\n * stdin → append-line → exit binary. They all write to the same per-\n * session JSONL at `<hooksDir>/<sessionId>.jsonl`. This module\n * concentrates the envelope shape and the atomic append so the per-\n * hook scripts stay <30 lines each.\n *\n * Hot-path constraints: NEVER block, NEVER network, NEVER read user\n * input. Pure stdin parse + filesystem append + exit. Worst-case one\n * `mkdir` + one `appendFile` per fire.\n */\n\nimport { appendFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { getHooksDir } from \"../config\";\n\n/** Wire-format envelope written one-per-line to the hook JSONL.\n * The watcher in `sessions.ts` parses this exact shape — keep in sync. */\nexport interface HookDropEnvelope<TData = unknown> {\n type: string;\n sessionId: string;\n ts: string;\n /** Idempotency key. Lets the watcher's replay-on-restart skip drops\n * whose effects were already emitted (see `HookCursor.applied`). */\n uniqId: string;\n data: TData;\n}\n\nexport interface AppendDropRequest<TData> {\n type: string;\n sessionId: string;\n uniqId: string;\n data: TData;\n}\n\n/** Append one envelope line to the per-session hook JSONL. Atomic between\n * hook processes via POSIX `O_APPEND` (Node's `fs.appendFile`) — concurrent\n * appends from different CC sessions / hook types can't interleave bytes\n * within a line as long as the line stays < `PIPE_BUF` (4KB). Drops are\n * <1KB in practice, so this is safe without locks.\n *\n * Stderr logs are written so `tail -f ~/.jarvis/hooks/hook.log` (when\n * CC pipes hook stderr there, which it does by default) shows what fired.\n * Stderr never breaks CC — it only watches stdout for decision JSON. */\nexport async function appendDrop<TData>(req: AppendDropRequest<TData>): Promise<void> {\n if (!req.sessionId || !req.type || !req.uniqId) {\n process.stderr.write(\n `[hook] skip ${req.type ?? \"?\"} missing field ` +\n `sessionId=${!!req.sessionId} uniqId=${!!req.uniqId}\\n`,\n );\n return;\n }\n const dir = getHooksDir();\n await mkdir(dir, { recursive: true }).catch((err) => {\n process.stderr.write(`[hook] mkdir failed ${dir}: ${err}\\n`);\n });\n const envelope: HookDropEnvelope<TData> = {\n type: req.type,\n sessionId: req.sessionId,\n ts: new Date().toISOString(),\n uniqId: req.uniqId,\n data: req.data,\n };\n const file = join(dir, `${req.sessionId}.jsonl`);\n try {\n await appendFile(file, JSON.stringify(envelope) + \"\\n\");\n process.stderr.write(\n `[hook] ${req.type} appended sessionId=${req.sessionId} uniqId=${req.uniqId}\\n`,\n );\n } catch (err) {\n process.stderr.write(`[hook] append failed ${file}: ${err}\\n`);\n }\n}\n\n/** Read CC's hook payload from stdin. Returns `null` on parse failure\n * (never throws — the hook must never block CC). */\nexport async function readHookStdin<T = Record<string, unknown>>(): Promise<T | null> {\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n }\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n if (!raw) return null;\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/** Hard cap any hook so an exotic stdin stall can't pile up forever\n * before CC's terminal prompt fires. We exit 0 with no output → CC\n * proceeds with its normal permission flow. */\nexport function installHookTimeout(ms = 750): void {\n const timer = setTimeout(() => process.exit(0), ms);\n timer.unref();\n}\n","/**\n * Agent Config\n *\n * Manages ~/.jarvis/config.json — saved by `jarvis connect <token>`,\n * read on `jarvis start`.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport os from \"os\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface AgentConfig {\n /** The jarvis this machine joined — the address a person opens in a browser,\n * and what pairing talks to. Ours by default; `jarvis init --url <url>` joins\n * someone else's. */\n url?: string;\n /** WS hub URL this machine dials. Written once, by pairing, from the hub the\n * joined instance named — and never from a flag, an environment variable or\n * any later command. Re-pointing a machine means unpairing it first. */\n hubUrl?: string;\n /** JWT auth token (from connect token) */\n token?: string;\n /** Long-lived refresh token for obtaining new access tokens */\n refreshToken?: string;\n /** User ID */\n userId: string;\n /** Environment ID */\n envId?: string;\n /** Workspace root path for repo operations */\n workspacePath?: string;\n /** Anthropic API key (for local-only use without cloud) */\n anthropicApiKey?: string;\n /** Use Anthropic subscription instead of API key */\n useSubscription?: boolean;\n /** When the config was last updated */\n connectedAt?: string;\n}\n\nexport interface ConnectToken {\n hubUrl: string;\n jwt: string;\n refreshToken: string;\n userId: string;\n envId: string;\n}\n\n// =============================================================================\n// Environment\n// =============================================================================\n\n/** The jarvis a machine pairs with when nothing else is named. Only ever read\n * by pairing: `jarvis init --url <url>` joins someone else's instead, and the\n * hub URL is whatever that instance hands back. */\nexport const DEFAULT_URL = \"https://jarvis.appchy.com\";\n\n// =============================================================================\n// Config directory\n// =============================================================================\n\n/** The config dir is the machine's identity: what it paired as, and therefore\n * which hub it dials. One daemon per dir, so a second identity on the same\n * machine — a storage rig, a localhost instance — is a second dir, named by\n * `JARVIS_CONFIG_DIR`, never a second binary.\n *\n * Called fresh on every read so a test that sets `JARVIS_CONFIG_DIR` AFTER\n * module load still sees the override — caching at module init would lock it\n * onto the host's real `~/.jarvis/` and leak production state into the\n * in-memory infra. */\nfunction configDir(): string {\n if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;\n return path.join(os.homedir(), \".jarvis\");\n}\n\nfunction configFile(): string {\n return path.join(configDir(), \"config.json\");\n}\n\n/** Absolute path to the per-daemon config directory — `~/.jarvis/` unless\n * `JARVIS_CONFIG_DIR` names another. */\nexport function getConfigDir(): string {\n return configDir();\n}\n\n/** Per-daemon directory for PreToolUse hook drop files. The CC hook script\n * writes `<sessionId>/<toolUseId>.json` here; the sessions watcher consumes\n * them. Each daemon owns the subtree under its own config dir, so a hook\n * installed for one never feeds another. */\nexport function getHooksDir(): string {\n return path.join(configDir(), \"hooks\");\n}\n\n// =============================================================================\n// Operations\n// =============================================================================\n\n/** Read the machine's config, accepting the names these two fields used to\n * carry. `appUrl`/`apiUrl` were renamed on 2026-08-15 — `apiUrl` never held an\n * API URL, it held the hub's. A config written before that still loads, and\n * `saveConfig` only ever writes the current names, so a machine heals itself on\n * its next write with nothing to run. Delete this tolerance once no config\n * predating the rename is in use. */\nexport function loadConfig(): AgentConfig | null {\n try {\n const stored = JSON.parse(fs.readFileSync(configFile(), \"utf-8\")) as AgentConfig &\n Partial<{ appUrl: string; apiUrl: string }>;\n const { appUrl, apiUrl, ...config } = stored;\n const url = config.url ?? appUrl;\n const hubUrl = config.hubUrl ?? apiUrl;\n return {\n ...config,\n ...(url !== undefined ? { url } : {}),\n ...(hubUrl !== undefined ? { hubUrl } : {}),\n };\n } catch {\n return null;\n }\n}\n\n/** Persist the machine's config.\n *\n * Refuses to move an already-paired machine to a different hub. A machine that\n * joins the wrong control plane hands it credentials, so re-pointing one is a\n * deliberate act — `jarvis unpair` (which clears the config) and then pair\n * again — rather than something a stale token or a mistyped command can do on\n * its way past. Re-pairing to the same hub is unaffected. */\nexport function saveConfig(config: AgentConfig): void {\n const paired = loadConfig();\n if (paired?.hubUrl && config.hubUrl && config.hubUrl !== paired.hubUrl) {\n throw new Error(\n `This machine is paired with ${paired.hubUrl} and cannot be moved to ${config.hubUrl}.\\n` +\n ` Run 'jarvis unpair' first if you mean to join a different jarvis.`,\n );\n }\n fs.mkdirSync(configDir(), { recursive: true });\n fs.writeFileSync(configFile(), JSON.stringify(config, null, 2) + \"\\n\");\n}\n\nexport function clearConfig(): void {\n try {\n fs.unlinkSync(configFile());\n } catch {}\n}\n\nexport function parseConnectToken(token: string): ConnectToken {\n try {\n const decoded = Buffer.from(token, \"base64\").toString(\"utf-8\");\n const parsed = JSON.parse(decoded);\n if (!parsed.hubUrl || !parsed.jwt || !parsed.userId) {\n throw new Error(\"Invalid token: missing required fields (hubUrl, jwt, userId)\");\n }\n return parsed as ConnectToken;\n } catch (err) {\n if (err instanceof SyntaxError) {\n throw new Error(\"Invalid token: not valid base64-encoded JSON\");\n }\n throw err;\n }\n}\n\nexport function getConfigPath(): string {\n return configFile();\n}\n","/**\n * UserPromptSubmit hook — drops an envelope each time a `claude` user submits\n * a prompt. Jarvis starts no sessions, so every drop observed here is a\n * person's turn.\n *\n * Used to flip `SessionCacheEntry.controller` to `\"claudeCode\"` on the\n * watcher side, surfacing the controller change in the web UI's pill.\n */\n\nimport { appendDrop, installHookTimeout, readHookStdin } from \"./drop\";\n\ninterface CcUserPromptSubmitInput {\n session_id?: string;\n transcript_path?: string;\n cwd?: string;\n prompt?: string;\n}\n\ninterface UserPromptSubmitDropData {\n transcriptPath?: string;\n cwd?: string;\n}\n\ninstallHookTimeout();\n\nasync function main(): Promise<void> {\n const p = await readHookStdin<CcUserPromptSubmitInput>();\n if (!p) return;\n const sessionId = p.session_id;\n if (!sessionId) return;\n\n const ts = new Date().toISOString();\n const data: UserPromptSubmitDropData = {\n ...(p.transcript_path ? { transcriptPath: p.transcript_path } : {}),\n ...(p.cwd ? { cwd: p.cwd } : {}),\n };\n\n await appendDrop<UserPromptSubmitDropData>({\n type: \"user-prompt-submit\",\n sessionId,\n uniqId: `user-prompt-submit-${ts}`,\n data,\n });\n}\n\nvoid main().finally(() => process.exit(0));\n"],"mappings":";;;AAcA,SAAS,YAAY,aAAa;AAClC,SAAS,YAAY;;;ACPrB,OAAO,UAAU;AACjB,OAAO,QAAQ;AA+Df,SAAS,YAAoB;AAC3B,MAAI,QAAQ,IAAI,kBAAmB,QAAO,QAAQ,IAAI;AACtD,SAAO,KAAK,KAAK,GAAG,QAAQ,GAAG,SAAS;AAC1C;AAgBO,SAAS,cAAsB;AACpC,SAAO,KAAK,KAAK,UAAU,GAAG,OAAO;AACvC;;;AD9CA,eAAsB,WAAkB,KAA8C;AACpF,MAAI,CAAC,IAAI,aAAa,CAAC,IAAI,QAAQ,CAAC,IAAI,QAAQ;AAC9C,YAAQ,OAAO;AAAA,MACb,eAAe,IAAI,QAAQ,GAAG,4BACf,CAAC,CAAC,IAAI,SAAS,WAAW,CAAC,CAAC,IAAI,MAAM;AAAA;AAAA,IACvD;AACA;AAAA,EACF;AACA,QAAM,MAAM,YAAY;AACxB,QAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC,EAAE,MAAM,CAAC,QAAQ;AACnD,YAAQ,OAAO,MAAM,uBAAuB,GAAG,KAAK,GAAG;AAAA,CAAI;AAAA,EAC7D,CAAC;AACD,QAAM,WAAoC;AAAA,IACxC,MAAM,IAAI;AAAA,IACV,WAAW,IAAI;AAAA,IACf,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC3B,QAAQ,IAAI;AAAA,IACZ,MAAM,IAAI;AAAA,EACZ;AACA,QAAM,OAAO,KAAK,KAAK,GAAG,IAAI,SAAS,QAAQ;AAC/C,MAAI;AACF,UAAM,WAAW,MAAM,KAAK,UAAU,QAAQ,IAAI,IAAI;AACtD,YAAQ,OAAO;AAAA,MACb,UAAU,IAAI,IAAI,uBAAuB,IAAI,SAAS,WAAW,IAAI,MAAM;AAAA;AAAA,IAC7E;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,OAAO,MAAM,wBAAwB,IAAI,KAAK,GAAG;AAAA,CAAI;AAAA,EAC/D;AACF;AAIA,eAAsB,gBAAgE;AACpF,MAAI;AACF,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,OAAO,UAAU,WAAW,OAAO,KAAK,KAAK,IAAI,KAAK;AAAA,IACpE;AACA,UAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO;AAClD,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,mBAAmB,KAAK,KAAW;AACjD,QAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK,CAAC,GAAG,EAAE;AAClD,QAAM,MAAM;AACd;;;AE5EA,mBAAmB;AAEnB,eAAe,OAAsB;AACnC,QAAM,IAAI,MAAM,cAAuC;AACvD,MAAI,CAAC,EAAG;AACR,QAAM,YAAY,EAAE;AACpB,MAAI,CAAC,UAAW;AAEhB,QAAM,MAAK,oBAAI,KAAK,GAAE,YAAY;AAClC,QAAM,OAAiC;AAAA,IACrC,GAAI,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,gBAAgB,IAAI,CAAC;AAAA,IACjE,GAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC;AAAA,EAChC;AAEA,QAAM,WAAqC;AAAA,IACzC,MAAM;AAAA,IACN;AAAA,IACA,QAAQ,sBAAsB,EAAE;AAAA,IAChC;AAAA,EACF,CAAC;AACH;AAEA,KAAK,KAAK,EAAE,QAAQ,MAAM,QAAQ,KAAK,CAAC,CAAC;","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# {{TITLE}} — technical architecture + phased plan
|
|
2
|
+
|
|
3
|
+
<!-- NOT a claude.ai/design UI brief (see design.md for that). This is the
|
|
4
|
+
version-level TECHNICAL brief — the cross-task "plan FROM this" doc: the
|
|
5
|
+
shared seams, the migration/build slices, the rules that must not
|
|
6
|
+
regress. Owner of record for the cross-task design; each task's own
|
|
7
|
+
`plan.md` still owns its own As-found + Architecture at pickup. -->
|
|
8
|
+
|
|
9
|
+
> Direction → <!-- rule id(s) -->
|
|
10
|
+
<!-- Cite the governing rule ids (+ any narrowing/superseding ones) and, if this
|
|
11
|
+
version has one, the research report it draws from. -->
|
|
12
|
+
|
|
13
|
+
## As-found
|
|
14
|
+
|
|
15
|
+
<!-- Raw, factual account of how the in-scope code works today — file:line,
|
|
16
|
+
confirmed by the blast-radius sweep. No opinions. What exists, what's
|
|
17
|
+
missing, where the
|
|
18
|
+
seams already are. -->
|
|
19
|
+
|
|
20
|
+
## Target architecture
|
|
21
|
+
|
|
22
|
+
<!-- Lead with a diagram, then the rationale (why this shape, alternatives
|
|
23
|
+
weighed). The shared seams/contracts every task in this version builds
|
|
24
|
+
against. -->
|
|
25
|
+
|
|
26
|
+
## Code map
|
|
27
|
+
|
|
28
|
+
<!-- Per `code` region this version touches — a MAP of seams/decisions/links,
|
|
29
|
+
NEVER a code dump. Where Pass-1 topology + the architect's foundation
|
|
30
|
+
reasoning land (SKILL.md §Two-pass decomposition). One block per region;
|
|
31
|
+
omit a region until the cut actually touches it. -->
|
|
32
|
+
|
|
33
|
+
### <code-region>
|
|
34
|
+
|
|
35
|
+
- **Seam** — <the shared contract/boundary here; link `path:line`, don't paste>
|
|
36
|
+
- **Known-next constraints** — <what later same-region tasks will need this seam
|
|
37
|
+
to support, so it isn't built too narrow>
|
|
38
|
+
- **Consumers** — <the features/tasks that build on this seam (≥2 for a real
|
|
39
|
+
foundation)>
|
|
40
|
+
- **Foundation task** — <the task that lands this seam first, if any; else "—">
|
|
41
|
+
|
|
42
|
+
## Phases
|
|
43
|
+
|
|
44
|
+
<!-- Each phase independently shippable (own acceptance, own commit) —
|
|
45
|
+
never a partial, unshippable slice. One subsection per phase:
|
|
46
|
+
name, what it delivers, what it depends on. -->
|
|
47
|
+
|
|
48
|
+
1. **<phase-name>** — <what ships, dependencies>
|
|
49
|
+
|
|
50
|
+
## Risks
|
|
51
|
+
|
|
52
|
+
<!-- Open items / risks to resolve per-phase, measure-first. What could
|
|
53
|
+
invalidate the target architecture; what to watch for. -->
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# {{TITLE}} — claude.ai/design brief
|
|
2
|
+
|
|
3
|
+
<!-- Paste this into https://claude.ai/design and link this repo. Claude Design
|
|
4
|
+
already has the full design context — the prototype in /wireframes/, the
|
|
5
|
+
codebase's components, architecture and styling.
|
|
6
|
+
|
|
7
|
+
Claude Design owns the "how" (layout, controls, copy, states). This brief
|
|
8
|
+
is the "what" — the version goal, the user stories, the audience. Don't
|
|
9
|
+
pre-spec screens or content shapes; let Claude Design propose them. -->
|
|
10
|
+
|
|
11
|
+
## Goal
|
|
12
|
+
|
|
13
|
+
<!-- One paragraph. The capability this version delivers in USER terms — what
|
|
14
|
+
can the user do after this ships that they cannot do today? Why does it
|
|
15
|
+
matter? No technical detail, no implementation hints. -->
|
|
16
|
+
|
|
17
|
+
## User stories
|
|
18
|
+
|
|
19
|
+
<!-- Lift from the feature stories of the features in this cut
|
|
20
|
+
(work/product/**.md §Stories — the durable owner; version stories.md
|
|
21
|
+
is retired). Each story in:
|
|
22
|
+
"As a <role>, I want <capability>, so that <outcome>."
|
|
23
|
+
followed by a short list of observable / verifiable behaviours it implies
|
|
24
|
+
(behaviours, not screens) — reference the feature's B-/AC- ids. -->
|
|
25
|
+
|
|
26
|
+
## Audience
|
|
27
|
+
|
|
28
|
+
<!-- Who the user is + what they already know — Claude Design needs this to
|
|
29
|
+
pitch affordances at the right level. One short paragraph. -->
|
|
30
|
+
|
|
31
|
+
## Workflow
|
|
32
|
+
|
|
33
|
+
Work in **two passes**, then clean up for export. Don't skip ahead — Pass 2
|
|
34
|
+
implements what Pass 1 defines.
|
|
35
|
+
|
|
36
|
+
### Pass 1 — Wireframes + behaviour
|
|
37
|
+
|
|
38
|
+
Sketch the screens at low fidelity (layout, structure, key controls) and
|
|
39
|
+
**define every behaviour alongside**. As you go, write the per-page spec to
|
|
40
|
+
`ui.md` in this exact format:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
# ui.md — {{TITLE}}
|
|
44
|
+
|
|
45
|
+
## Page: <name>
|
|
46
|
+
|
|
47
|
+
### Layout
|
|
48
|
+
<ASCII sketch: before> <ASCII sketch: after>
|
|
49
|
+
|
|
50
|
+
### Changed elements
|
|
51
|
+
#### <element>
|
|
52
|
+
- Does — <behaviour: what happens on interaction, what it reflects>
|
|
53
|
+
- Looks — <visual states + variants, where it sits>
|
|
54
|
+
- Relates to — <other elements/states it depends on or drives>
|
|
55
|
+
- Blast radius — <sibling components, broker/protocol, persisted state>
|
|
56
|
+
|
|
57
|
+
## Backend changes deduced
|
|
58
|
+
- <what the client behaviour implies the broker / protocol / event model
|
|
59
|
+
must provide — the bridge into the implementation phase>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
One section per page that changes; scope tightly (only changed elements
|
|
63
|
+
appear, never untouched ones). Skip the "before" sketch for a brand-new page;
|
|
64
|
+
skip the whole Layout block when the frame is unchanged and only inner
|
|
65
|
+
elements moved. The Backend changes section at the end is mandatory — it is
|
|
66
|
+
how the implementation phase finds its inputs.
|
|
67
|
+
|
|
68
|
+
Iterate Pass 1 until every page's behaviour is settled in `ui.md`. Don't
|
|
69
|
+
start the visual polish before this point.
|
|
70
|
+
|
|
71
|
+
### Pass 2 — Live mock
|
|
72
|
+
|
|
73
|
+
Implement what Pass 1 defined. Build the **polished HTML/CSS/JS prototype**,
|
|
74
|
+
realising the behaviours from `ui.md` with the final visual style. The mock
|
|
75
|
+
is the behavioural + visual source of truth.
|
|
76
|
+
|
|
77
|
+
### Cleanup for export
|
|
78
|
+
|
|
79
|
+
When the prototype is final:
|
|
80
|
+
|
|
81
|
+
- **Delete old screenshots** captured during iteration.
|
|
82
|
+
- **Re-take fresh screenshots** of every page in the final mock.
|
|
83
|
+
- Re-check `ui.md` against the prototype (behaviour may have refined during
|
|
84
|
+
the polish pass) and update it if it drifted.
|
|
85
|
+
- The project is **export-ready** — no dead files, no half-built states.
|
|
86
|
+
|
|
87
|
+
The exported bundle (`project/` + `ui.md` + screenshots) drops into
|
|
88
|
+
`/wireframes/` in the repo. The work skill takes it from there to decompose
|
|
89
|
+
into tasks.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: domain
|
|
3
|
+
id: architecture
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Architecture — how it is built
|
|
7
|
+
|
|
8
|
+
## What belongs here
|
|
9
|
+
|
|
10
|
+
Durable calls about **how the software is built** — the systems, their seams, the
|
|
11
|
+
dependency rules, and the constraints a plan must not regress.
|
|
12
|
+
|
|
13
|
+
Rules here live in `architecture/<system>.md`, not in this README: a system is a
|
|
14
|
+
capability boundary that owns `code:` regions and hosts the rules governing them.
|
|
15
|
+
This README is the domain's landing doc and carries the derived system index and
|
|
16
|
+
dependency map. A region belongs to exactly ONE system.
|
|
17
|
+
|
|
18
|
+
Its neighbours: [`product/`](../product/README.md) owns what the software must do —
|
|
19
|
+
this domain owns the shape that delivers it.
|
|
20
|
+
[`operations/`](../operations/README.md) owns getting it out and keeping it running;
|
|
21
|
+
a build-time rule is this domain's, a deploy-time one is theirs.
|
|
22
|
+
[`security/`](../security/README.md) owns what must stay safe while it runs.
|
|
23
|
+
|
|
24
|
+
## Non-goals
|
|
25
|
+
|
|
26
|
+
- **What the software does.** `product/`.
|
|
27
|
+
- **How it ships and runs.** `operations/`.
|
|
28
|
+
- **A rule about a kind of call rather than a system.** It belongs to the domain that owns that kind — filing it here because it was the only place that fit is the mis-filing the nine domains exist to end.
|
|
29
|
+
- **Package inventory.** Derivable from the file tree; a system is not one-per-package.
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
|
|
33
|
+
<!-- A durable rule lives in the domain that owns its KIND of call. Shape:
|
|
34
|
+
|
|
35
|
+
### <id> — the rule, in one line
|
|
36
|
+
enforced_by: the check that catches a violation — a command, a test, a lint
|
|
37
|
+
|
|
38
|
+
`enforced_by: judgement` is legal where a person really must decide, and is
|
|
39
|
+
counted separately rather than treated as enforcement. An empty one is a wish.
|
|
40
|
+
|
|
41
|
+
This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
|
|
42
|
+
rules to make the domain look occupied — an empty domain is a signpost saying
|
|
43
|
+
where a call will go when you make one. -->
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: domain
|
|
3
|
+
id: commercial
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Commercial — how we reach people and earn
|
|
7
|
+
|
|
8
|
+
## What belongs here
|
|
9
|
+
|
|
10
|
+
Durable calls about **reach and revenue** — pricing shape, packaging and tiers,
|
|
11
|
+
positioning, distribution and partners, and how growth is measured.
|
|
12
|
+
|
|
13
|
+
Expect many rules here to be `enforced_by: judgement`, and that is honest rather
|
|
14
|
+
than lazy: a positioning call is not something a check can catch. They are still
|
|
15
|
+
written down and still counted, because the alternative is that they live only in
|
|
16
|
+
somebody's head and get silently re-argued.
|
|
17
|
+
|
|
18
|
+
Its neighbours: [`product/`](../product/README.md) owns what the thing does — this
|
|
19
|
+
domain owns who it is for and what it costs. [`legal/`](../legal/README.md) owns
|
|
20
|
+
what a commitment binds us to. [`design/`](../design/README.md) owns the surface;
|
|
21
|
+
brand *reach* is this domain's, brand *rendering* is theirs.
|
|
22
|
+
|
|
23
|
+
## Non-goals
|
|
24
|
+
|
|
25
|
+
- **What the product does.** `product/`.
|
|
26
|
+
- **What a contract obliges.** `legal/`.
|
|
27
|
+
- **How it is delivered technically.** `operations/`.
|
|
28
|
+
- **A campaign.** That is work; the pricing rule behind it is durable.
|
|
29
|
+
|
|
30
|
+
## Rules
|
|
31
|
+
|
|
32
|
+
<!-- A durable rule lives in the domain that owns its KIND of call. Shape:
|
|
33
|
+
|
|
34
|
+
### <id> — the rule, in one line
|
|
35
|
+
enforced_by: the check that catches a violation — a command, a test, a lint
|
|
36
|
+
|
|
37
|
+
`enforced_by: judgement` is legal where a person really must decide, and is
|
|
38
|
+
counted separately rather than treated as enforcement. An empty one is a wish.
|
|
39
|
+
|
|
40
|
+
This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
|
|
41
|
+
rules to make the domain look occupied — an empty domain is a signpost saying
|
|
42
|
+
where a call will go when you make one. -->
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: domain
|
|
3
|
+
id: design
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Design — how it looks and reads
|
|
7
|
+
|
|
8
|
+
## What belongs here
|
|
9
|
+
|
|
10
|
+
Durable calls about the **visual and interaction surface** — layout, type, colour,
|
|
11
|
+
motion, component behaviour, tone of the words on screen, and accessibility.
|
|
12
|
+
|
|
13
|
+
A note on the word, because it costs sessions: **"design" unqualified means this
|
|
14
|
+
domain**. A technical plan is "the plan", never "the design", however architectural
|
|
15
|
+
it is. A repo that lets one word mean both ends up with two documents claiming to
|
|
16
|
+
be the design of the same change.
|
|
17
|
+
|
|
18
|
+
Its neighbours: [`product/`](../product/README.md) owns what a user must be able to
|
|
19
|
+
do — this domain owns what that looks and feels like.
|
|
20
|
+
[`support/`](../support/README.md) owns getting a stuck user moving; where a design
|
|
21
|
+
choice is the thing that stuck them, the fix is this domain's.
|
|
22
|
+
|
|
23
|
+
## Non-goals
|
|
24
|
+
|
|
25
|
+
- **What the feature must do.** `product/`.
|
|
26
|
+
- **The technical plan.** `architecture/`, and it is called the plan.
|
|
27
|
+
- **Brand reach and positioning.** `commercial/`.
|
|
28
|
+
- **One screen's spec.** That is a feature's `<feature>.md`, or a task's UI brief.
|
|
29
|
+
|
|
30
|
+
## Rules
|
|
31
|
+
|
|
32
|
+
<!-- A durable rule lives in the domain that owns its KIND of call. Shape:
|
|
33
|
+
|
|
34
|
+
### <id> — the rule, in one line
|
|
35
|
+
enforced_by: the check that catches a violation — a command, a test, a lint
|
|
36
|
+
|
|
37
|
+
`enforced_by: judgement` is legal where a person really must decide, and is
|
|
38
|
+
counted separately rather than treated as enforcement. An empty one is a wish.
|
|
39
|
+
|
|
40
|
+
This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
|
|
41
|
+
rules to make the domain look occupied — an empty domain is a signpost saying
|
|
42
|
+
where a call will go when you make one. -->
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: domain
|
|
3
|
+
id: legal
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Legal — what binds us
|
|
7
|
+
|
|
8
|
+
## What belongs here
|
|
9
|
+
|
|
10
|
+
Durable calls about **obligation** — licensing of what we ship and what we depend
|
|
11
|
+
on, attribution, terms and privacy commitments, regulatory constraints, and what a
|
|
12
|
+
published artifact must and must not claim.
|
|
13
|
+
|
|
14
|
+
Its neighbours: [`security/`](../security/README.md) owns technical safety — a
|
|
15
|
+
credential rule is theirs, a data-retention *commitment* is this domain's.
|
|
16
|
+
[`commercial/`](../commercial/README.md) owns the deal; this domain owns what
|
|
17
|
+
agreeing to it binds us to.
|
|
18
|
+
|
|
19
|
+
**Often empty in a young repo, and it ships anyway** — see the note in
|
|
20
|
+
[`security/`](../security/README.md). The first rule that lands here is usually a
|
|
21
|
+
dependency licence that turned out to constrain distribution, and it is much better
|
|
22
|
+
found by a charter that already exists than by a lawyer.
|
|
23
|
+
|
|
24
|
+
## Non-goals
|
|
25
|
+
|
|
26
|
+
- **Technical safety measures.** `security/`.
|
|
27
|
+
- **Pricing and packaging.** `commercial/`.
|
|
28
|
+
- **How we run it.** `operations/`.
|
|
29
|
+
- **One licence review.** That is work; the rule it establishes is durable.
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
|
|
33
|
+
<!-- A durable rule lives in the domain that owns its KIND of call. Shape:
|
|
34
|
+
|
|
35
|
+
### <id> — the rule, in one line
|
|
36
|
+
enforced_by: the check that catches a violation — a command, a test, a lint
|
|
37
|
+
|
|
38
|
+
`enforced_by: judgement` is legal where a person really must decide, and is
|
|
39
|
+
counted separately rather than treated as enforcement. An empty one is a wish.
|
|
40
|
+
|
|
41
|
+
This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
|
|
42
|
+
rules to make the domain look occupied — an empty domain is a signpost saying
|
|
43
|
+
where a call will go when you make one. -->
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: domain
|
|
3
|
+
id: operations
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Operations — how we ship it and run it
|
|
7
|
+
|
|
8
|
+
## What belongs here
|
|
9
|
+
|
|
10
|
+
Durable calls about **getting it out and keeping it running** — how something is
|
|
11
|
+
published, how a consumer installs and updates, what a version means, how the
|
|
12
|
+
release sequence works, and how the repo's own tooling runs.
|
|
13
|
+
|
|
14
|
+
Its neighbours: [`security/`](../security/README.md) owns what must stay safe while
|
|
15
|
+
we do it — a scan of a published artifact is a security rule, the publish path
|
|
16
|
+
itself is this domain's. [`support/`](../support/README.md) owns keeping the *user*
|
|
17
|
+
unblocked; this domain owns keeping the *system* shipping.
|
|
18
|
+
[`architecture/`](../architecture/README.md) owns build-time structure.
|
|
19
|
+
|
|
20
|
+
## Non-goals
|
|
21
|
+
|
|
22
|
+
- **What the software does.** `product/`.
|
|
23
|
+
- **How it is structured internally.** `architecture/`.
|
|
24
|
+
- **Pricing and reach.** `commercial/`.
|
|
25
|
+
- **One incident.** A postmortem is a document; the rule it produces belongs here.
|
|
26
|
+
|
|
27
|
+
## Rules
|
|
28
|
+
|
|
29
|
+
<!-- A durable rule lives in the domain that owns its KIND of call. Shape:
|
|
30
|
+
|
|
31
|
+
### <id> — the rule, in one line
|
|
32
|
+
enforced_by: the check that catches a violation — a command, a test, a lint
|
|
33
|
+
|
|
34
|
+
`enforced_by: judgement` is legal where a person really must decide, and is
|
|
35
|
+
counted separately rather than treated as enforcement. An empty one is a wish.
|
|
36
|
+
|
|
37
|
+
This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
|
|
38
|
+
rules to make the domain look occupied — an empty domain is a signpost saying
|
|
39
|
+
where a call will go when you make one. -->
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: domain
|
|
3
|
+
id: product
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Product — what it does
|
|
7
|
+
|
|
8
|
+
## What belongs here
|
|
9
|
+
|
|
10
|
+
Durable calls about **what the thing does** — the behaviour a user can rely on, and
|
|
11
|
+
what the product *is*.
|
|
12
|
+
|
|
13
|
+
This domain is the one that holds two kinds of file. The `<feature>.md` behaviour
|
|
14
|
+
specs sit beside this charter and own stories, behaviour and `AC-nn`. This README
|
|
15
|
+
also hosts `## Rules` like any other domain, because those are different facts: a
|
|
16
|
+
feature spec says what the app does *today* and can be retired, while a product rule
|
|
17
|
+
says what the product *is* and outlives any feature that expresses it.
|
|
18
|
+
|
|
19
|
+
Its neighbours: [`design/`](../design/README.md) owns how it looks and reads —
|
|
20
|
+
this domain owns what it must do. [`architecture/`](../architecture/README.md) owns
|
|
21
|
+
how it is built. [`quality/`](../quality/README.md) owns how we know it works; a
|
|
22
|
+
criterion's *text* is a feature's, the *standard of proof* is quality's.
|
|
23
|
+
|
|
24
|
+
## Non-goals
|
|
25
|
+
|
|
26
|
+
- **How it is built.** `architecture/`.
|
|
27
|
+
- **How it looks.** `design/`.
|
|
28
|
+
- **Whether it works.** `quality/` owns the standard of evidence; the `AC-nn` text stays in `<feature>.md`.
|
|
29
|
+
- **Task-level scope.** A task brief is not durable direction.
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
|
|
33
|
+
<!-- A durable rule lives in the domain that owns its KIND of call. Shape:
|
|
34
|
+
|
|
35
|
+
### <id> — the rule, in one line
|
|
36
|
+
enforced_by: the check that catches a violation — a command, a test, a lint
|
|
37
|
+
|
|
38
|
+
`enforced_by: judgement` is legal where a person really must decide, and is
|
|
39
|
+
counted separately rather than treated as enforcement. An empty one is a wish.
|
|
40
|
+
|
|
41
|
+
This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
|
|
42
|
+
rules to make the domain look occupied — an empty domain is a signpost saying
|
|
43
|
+
where a call will go when you make one. -->
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: domain
|
|
3
|
+
id: quality
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Quality — how we know it is good
|
|
7
|
+
|
|
8
|
+
## What belongs here
|
|
9
|
+
|
|
10
|
+
Durable calls about **evidence** — what counts as proof that something works, how a
|
|
11
|
+
test binds to the criterion it proves, what the levels of proof are, and what makes
|
|
12
|
+
a green run trustworthy.
|
|
13
|
+
|
|
14
|
+
The distinction that earns this domain its own home: *what the app should do* is
|
|
15
|
+
product's, *whether it does* is this domain's. A rule that says "a criterion is
|
|
16
|
+
proven by a tag-bound passing test, never by a comment" is not a build rule and not
|
|
17
|
+
a behaviour spec — and before the domains split, it had nowhere to live.
|
|
18
|
+
|
|
19
|
+
Its neighbours: [`architecture/`](../architecture/README.md) owns how code is
|
|
20
|
+
built; this domain owns how we know the result is right.
|
|
21
|
+
[`product/`](../product/README.md) owns the `AC-nn` text this domain sets the
|
|
22
|
+
standard of proof for.
|
|
23
|
+
|
|
24
|
+
## Non-goals
|
|
25
|
+
|
|
26
|
+
- **The behaviour being proven.** `product/`.
|
|
27
|
+
- **How the code is structured.** `architecture/`.
|
|
28
|
+
- **Whether the deploy succeeded.** `operations/`.
|
|
29
|
+
- **A single test's assertions.** Code, not governance.
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
|
|
33
|
+
<!-- A durable rule lives in the domain that owns its KIND of call. Shape:
|
|
34
|
+
|
|
35
|
+
### <id> — the rule, in one line
|
|
36
|
+
enforced_by: the check that catches a violation — a command, a test, a lint
|
|
37
|
+
|
|
38
|
+
`enforced_by: judgement` is legal where a person really must decide, and is
|
|
39
|
+
counted separately rather than treated as enforcement. An empty one is a wish.
|
|
40
|
+
|
|
41
|
+
This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
|
|
42
|
+
rules to make the domain look occupied — an empty domain is a signpost saying
|
|
43
|
+
where a call will go when you make one. -->
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: domain
|
|
3
|
+
id: security
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Security — what keeps it safe
|
|
7
|
+
|
|
8
|
+
## What belongs here
|
|
9
|
+
|
|
10
|
+
Durable calls about **what must stay safe** — secrets and how they are held,
|
|
11
|
+
authentication and authorization boundaries, what untrusted input is and where it is
|
|
12
|
+
trusted, data handling, and what must never enter a published artifact.
|
|
13
|
+
|
|
14
|
+
Its neighbours: [`operations/`](../operations/README.md) owns the publish path and
|
|
15
|
+
keeping the system running — this domain owns what must remain true while it does.
|
|
16
|
+
[`legal/`](../legal/README.md) owns what binds us contractually; a rule about
|
|
17
|
+
technical safety is this domain's, one about obligation is theirs.
|
|
18
|
+
[`architecture/`](../architecture/README.md) owns the seams; where a seam exists
|
|
19
|
+
*because* of a trust boundary, cite this domain's rule from it.
|
|
20
|
+
|
|
21
|
+
**This domain is often empty in a young repo, and that is fine.** It ships anyway,
|
|
22
|
+
because the cost of an empty charter is one file and the cost of a missing domain is
|
|
23
|
+
a security rule mis-filed under whichever system happened to be nearby.
|
|
24
|
+
|
|
25
|
+
## Non-goals
|
|
26
|
+
|
|
27
|
+
- **How it is deployed.** `operations/`.
|
|
28
|
+
- **Contractual and regulatory obligation.** `legal/`.
|
|
29
|
+
- **Whether the feature works.** `quality/`.
|
|
30
|
+
- **A specific vulnerability.** That is a task, not a durable rule — though the rule that prevents its class belongs here.
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
<!-- A durable rule lives in the domain that owns its KIND of call. Shape:
|
|
35
|
+
|
|
36
|
+
### <id> — the rule, in one line
|
|
37
|
+
enforced_by: the check that catches a violation — a command, a test, a lint
|
|
38
|
+
|
|
39
|
+
`enforced_by: judgement` is legal where a person really must decide, and is
|
|
40
|
+
counted separately rather than treated as enforcement. An empty one is a wish.
|
|
41
|
+
|
|
42
|
+
This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
|
|
43
|
+
rules to make the domain look occupied — an empty domain is a signpost saying
|
|
44
|
+
where a call will go when you make one. -->
|