@aiwg/cli 2026.10.5 → 2026.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +3 -1
  6. package/dist/src/artifacts/browser-export.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +8 -1
  8. package/dist/src/artifacts/cli.js +57 -6
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  10. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  11. package/dist/src/artifacts/graph-query.js +18 -8
  12. package/dist/src/artifacts/index-adapters.js +339 -0
  13. package/dist/src/artifacts/index-builder.js +143 -14
  14. package/dist/src/artifacts/index-files.js +15 -4
  15. package/dist/src/artifacts/index-reader.js +44 -0
  16. package/dist/src/artifacts/index-refresh.js +114 -55
  17. package/dist/src/artifacts/index-status.js +20 -5
  18. package/dist/src/artifacts/memory-maintenance.js +503 -0
  19. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  20. package/dist/src/artifacts/query-engine.js +263 -108
  21. package/dist/src/artifacts/relations-builder.js +2 -2
  22. package/dist/src/artifacts/source-graph.js +2 -2
  23. package/dist/src/artifacts/types.js +9 -1
  24. package/dist/src/cli/handlers/decision.js +19 -1
  25. package/dist/src/cli/handlers/guided-setup.js +470 -0
  26. package/dist/src/cli/handlers/help.js +2 -2
  27. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  28. package/dist/src/cli/handlers/index.js +4 -2
  29. package/dist/src/cli/handlers/init.js +41 -6
  30. package/dist/src/cli/handlers/local-executor.js +5 -3
  31. package/dist/src/cli/handlers/mc.js +509 -53
  32. package/dist/src/cli/handlers/models.js +32 -2
  33. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  34. package/dist/src/cli/handlers/rules.js +1 -1
  35. package/dist/src/cli/handlers/script-runner.js +73 -2
  36. package/dist/src/cli/handlers/serve.js +5 -3
  37. package/dist/src/cli/handlers/sessions.js +1 -0
  38. package/dist/src/cli/handlers/setup.js +428 -1
  39. package/dist/src/cli/handlers/skill-lint.js +1 -1
  40. package/dist/src/cli/handlers/subcommands.js +118 -8
  41. package/dist/src/cli/handlers/use.js +279 -93
  42. package/dist/src/cli/handlers/workspace.js +3 -1
  43. package/dist/src/cli/prompt-utils.js +8 -0
  44. package/dist/src/cli/router.js +16 -1
  45. package/dist/src/config/aiwg-config.js +42 -0
  46. package/dist/src/config/cli.js +7 -3
  47. package/dist/src/config/decision-config.js +14 -1
  48. package/dist/src/config/native-sandbox-warnings.js +25 -0
  49. package/dist/src/contributors/validation.js +6 -5
  50. package/dist/src/decision/admission.js +7 -3
  51. package/dist/src/decision/benchmark.js +469 -0
  52. package/dist/src/decision/ledger.js +22 -3
  53. package/dist/src/decision/pricing.js +147 -0
  54. package/dist/src/decision/qualification/gate-evidence.js +18 -15
  55. package/dist/src/decision/qualification/load-staged.js +539 -0
  56. package/dist/src/decision/qualification/load.js +109 -33
  57. package/dist/src/decision/qualification/runner.js +5 -2
  58. package/dist/src/decision/review/service.js +6 -4
  59. package/dist/src/decision/review/validate.js +23 -2
  60. package/dist/src/decision/savings.js +8 -3
  61. package/dist/src/discovery/index.js +492 -0
  62. package/dist/src/discovery/interview.js +233 -0
  63. package/dist/src/discovery/plan-apply.js +571 -0
  64. package/dist/src/discovery/plan-store.js +171 -0
  65. package/dist/src/discovery/plan-types.js +2 -0
  66. package/dist/src/discovery/plan-validation.js +139 -0
  67. package/dist/src/discovery/types.js +2 -0
  68. package/dist/src/extensions/commands/definitions.js +49 -9
  69. package/dist/src/extensions/manifest.js +71 -44
  70. package/dist/src/extensions/project-local-discovery.js +1 -1
  71. package/dist/src/extensions/project-quickref.js +18 -17
  72. package/dist/src/extensions/validation.js +32 -29
  73. package/dist/src/housekeeper/daemon-config.js +148 -0
  74. package/dist/src/housekeeper/ipc.js +84 -0
  75. package/dist/src/housekeeper/lifecycle.js +148 -0
  76. package/dist/src/housekeeper/platform.js +35 -0
  77. package/dist/src/housekeeper/status.js +70 -0
  78. package/dist/src/housekeeper/units.js +114 -0
  79. package/dist/src/jobs/flow.js +22 -21
  80. package/dist/src/lint/rule-activation.js +20 -5
  81. package/dist/src/lint/runner.js +1 -1
  82. package/dist/src/mcp/cli.mjs +5 -6
  83. package/dist/src/mcp/config-file.mjs +6 -2
  84. package/dist/src/mcp/config-layers.mjs +23 -7
  85. package/dist/src/mcp/profiles.js +18 -31
  86. package/dist/src/mcp/profiles.mjs +18 -32
  87. package/dist/src/mcp/registry.js +7 -27
  88. package/dist/src/mcp/registry.mjs +7 -28
  89. package/dist/src/mcp/server.mjs +34 -1
  90. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  91. package/dist/src/mcp/tools/command-run.mjs +3 -2
  92. package/dist/src/mcp/tools/decision.mjs +49 -48
  93. package/dist/src/mcp/tools/discovery.mjs +46 -21
  94. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  95. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  96. package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
  97. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  98. package/dist/src/memory/audit.js +719 -0
  99. package/dist/src/memory/canonical-context.js +392 -13
  100. package/dist/src/memory/canonical-maintenance.js +95 -0
  101. package/dist/src/memory/cli-result.js +22 -0
  102. package/dist/src/memory/cli.js +176 -0
  103. package/dist/src/memory/context-pack.js +57 -6
  104. package/dist/src/memory/cost.js +512 -0
  105. package/dist/src/memory/firewall-classification.d.mts +7 -0
  106. package/dist/src/memory/firewall-classification.mjs +32 -0
  107. package/dist/src/memory/index.js +8 -0
  108. package/dist/src/memory/line-maintenance.js +68 -0
  109. package/dist/src/memory/maintenance-cli.js +286 -0
  110. package/dist/src/memory/maintenance-contract.js +136 -0
  111. package/dist/src/memory/maintenance-index.js +18 -0
  112. package/dist/src/memory/maintenance-owner.js +2 -0
  113. package/dist/src/memory/maintenance-record.js +138 -0
  114. package/dist/src/memory/maintenance.js +254 -0
  115. package/dist/src/memory/provider-memory-adapter.js +154 -0
  116. package/dist/src/memory/recommend-cli.js +83 -0
  117. package/dist/src/memory/recommend.js +269 -0
  118. package/dist/src/memory/scheduled.js +81 -0
  119. package/dist/src/memory/taxonomy.js +44 -0
  120. package/dist/src/mission-protocol/codecs.js +19 -9
  121. package/dist/src/models/config-loader.js +11 -1
  122. package/dist/src/models/model-capabilities.v1.json +74 -50
  123. package/dist/src/models/provider-policy.js +125 -62
  124. package/dist/src/models/resolver.js +45 -8
  125. package/dist/src/models/worker-installation.d.mts +7 -0
  126. package/dist/src/models/worker-installation.mjs +169 -37
  127. package/dist/src/models/wrapper-route.js +1 -1
  128. package/dist/src/output-modes/registry.js +8 -7
  129. package/dist/src/plugin/metadata-validator.js +1 -1
  130. package/dist/src/providers/deploy-version.d.mts +1 -0
  131. package/dist/src/providers/deploy-version.mjs +17 -0
  132. package/dist/src/providers/provider-definitions.js +19 -16
  133. package/dist/src/providers/skill-ownership.d.mts +11 -2
  134. package/dist/src/providers/skill-ownership.mjs +35 -17
  135. package/dist/src/rules/activation.d.mts +11 -2
  136. package/dist/src/rules/activation.mjs +82 -6
  137. package/dist/src/rules/inventory.js +1 -1
  138. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  139. package/dist/src/schema/zod-validation.d.mts +6 -0
  140. package/dist/src/schema/zod-validation.mjs +50 -0
  141. package/dist/src/serve/executor-registry.js +297 -20
  142. package/dist/src/serve/executor-token.d.mts +1 -0
  143. package/dist/src/serve/executor-token.mjs +8 -0
  144. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  145. package/dist/src/serve/worker-invocation.js +114 -15
  146. package/dist/src/sessions/adapters/claude.js +19 -18
  147. package/dist/src/sessions/adapters/codex.js +10 -9
  148. package/dist/src/sessions/adapters/copilot.js +14 -13
  149. package/dist/src/sessions/adapters/cursor.js +19 -18
  150. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  151. package/dist/src/sessions/adapters/factory.js +11 -10
  152. package/dist/src/sessions/adapters/generic.js +32 -23
  153. package/dist/src/sessions/adapters/grokbot.js +35 -35
  154. package/dist/src/sessions/adapters/hermes.js +20 -19
  155. package/dist/src/sessions/adapters/muse.js +50 -49
  156. package/dist/src/sessions/adapters/omp.js +5 -4
  157. package/dist/src/sessions/adapters/openclaw.js +30 -29
  158. package/dist/src/sessions/adapters/opencode.js +26 -26
  159. package/dist/src/sessions/adapters/openhuman.js +24 -23
  160. package/dist/src/sessions/adapters/pi.js +5 -4
  161. package/dist/src/sessions/adapters/windsurf.js +8 -8
  162. package/dist/src/sessions/analytics.js +10 -9
  163. package/dist/src/sessions/candidates.js +7 -6
  164. package/dist/src/sessions/contracts.js +63 -53
  165. package/dist/src/sessions/output-registration.js +11 -10
  166. package/dist/src/sessions/workspace-discovery.js +3 -4
  167. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  168. package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
  169. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  170. package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
  171. package/dist/src/update/checker.d.mts +4 -0
  172. package/dist/src/update/checker.mjs +14 -0
  173. package/dist/src/writing/exemplar-selection.js +6 -6
  174. package/dist/src/writing/voice-evaluation.js +24 -23
  175. package/dist/src/writing/writer-migration.js +14 -13
  176. package/dist/src/writing/writer-profile.js +19 -18
  177. package/dist/src/writing/writing-brief.js +18 -17
  178. package/dist/src/writing/writing-receipt.js +47 -46
  179. package/package.json +5 -2
  180. package/schemas/decision/DecisionReview.schema.json +78 -1
  181. package/schemas/mission-v1.schema.json +3 -0
  182. package/tools/agents/deploy-agents.mjs +67 -39
  183. package/tools/agents/providers/antigravity.mjs +6 -1
  184. package/tools/agents/providers/base.mjs +19 -13
  185. package/tools/agents/providers/claude.mjs +1 -1
  186. package/tools/agents/providers/codex.mjs +50 -56
  187. package/tools/agents/providers/copilot.mjs +8 -2
  188. package/tools/agents/providers/cursor.mjs +1 -1
  189. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  190. package/tools/agents/providers/factory.mjs +7 -1
  191. package/tools/agents/providers/grok-build.mjs +18 -2
  192. package/tools/agents/providers/grokbot.mjs +1 -1
  193. package/tools/agents/providers/hermes.mjs +7 -13
  194. package/tools/agents/providers/muse-hooks.mjs +21 -14
  195. package/tools/agents/providers/muse.mjs +7 -2
  196. package/tools/agents/providers/omp.mjs +28 -4
  197. package/tools/agents/providers/opencode.mjs +14 -5
  198. package/tools/agents/providers/openhuman.mjs +1 -1
  199. package/tools/agents/providers/pi.mjs +1 -1
  200. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  201. package/tools/agents/providers/rule-delivery.mjs +263 -71
  202. package/tools/agents/providers/windsurf.mjs +7 -0
  203. package/tools/cli/workspace-status.mjs +5 -0
  204. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  205. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  206. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  207. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  208. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  209. package/tools/ralph-external/process-monitor.mjs +7 -4
  210. package/tools/ralph-external/session-launcher.mjs +280 -32
  211. package/tools/skills/deploy-skills-codex.mjs +11 -7
  212. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Minimal JSON-RPC 2.0 client for the housekeeper daemon socket
3
+ * (epic #3353 slice B5).
4
+ *
5
+ * Speaks the same newline-delimited JSON framing as
6
+ * `tools/daemon/ipc-server.mjs` without importing `tools/` — `src/` must
7
+ * never import `tools/` (tools import compiled src via `importImpl`).
8
+ */
9
+ import net from 'node:net';
10
+ export async function requestDaemon(socketPath, method, options = {}) {
11
+ const timeoutMs = options.timeoutMs ?? 10_000;
12
+ return new Promise((resolve, reject) => {
13
+ let settled = false;
14
+ const fail = (error) => {
15
+ if (settled)
16
+ return;
17
+ settled = true;
18
+ try {
19
+ socket.destroy();
20
+ }
21
+ catch { /* already closed */ }
22
+ reject(error);
23
+ };
24
+ const succeed = (value) => {
25
+ if (settled)
26
+ return;
27
+ settled = true;
28
+ try {
29
+ socket.end();
30
+ }
31
+ catch { /* already closed */ }
32
+ resolve(value);
33
+ };
34
+ const timer = setTimeout(() => {
35
+ fail(new Error(`Daemon request timed out: ${method} (${timeoutMs}ms)`));
36
+ }, timeoutMs);
37
+ timer.unref?.();
38
+ let buffer = '';
39
+ const socket = net.createConnection(socketPath);
40
+ socket.once('connect', () => {
41
+ socket.write(`${JSON.stringify({
42
+ jsonrpc: '2.0', id: 1, method, params: options.params ?? {},
43
+ })}\n`);
44
+ });
45
+ socket.on('data', (chunk) => {
46
+ buffer += chunk.toString('utf8');
47
+ const newline = buffer.indexOf('\n');
48
+ if (newline === -1)
49
+ return;
50
+ const line = buffer.slice(0, newline).trim();
51
+ buffer = buffer.slice(newline + 1);
52
+ if (!line)
53
+ return;
54
+ clearTimeout(timer);
55
+ let response;
56
+ try {
57
+ response = JSON.parse(line);
58
+ }
59
+ catch {
60
+ fail(new Error(`Daemon returned invalid JSON for ${method}`));
61
+ return;
62
+ }
63
+ if (response.error) {
64
+ fail(new Error(`Daemon error for ${method}: ${response.error.message ?? 'unknown error'}`));
65
+ return;
66
+ }
67
+ succeed(response.result);
68
+ });
69
+ socket.once('error', (error) => {
70
+ clearTimeout(timer);
71
+ const code = error?.code;
72
+ if (code === 'ENOENT' || code === 'ECONNREFUSED') {
73
+ fail(new Error('Daemon is not running (socket unavailable)'));
74
+ return;
75
+ }
76
+ fail(error);
77
+ });
78
+ socket.once('close', () => {
79
+ clearTimeout(timer);
80
+ fail(new Error(`Daemon connection closed during ${method}`));
81
+ });
82
+ });
83
+ }
84
+ //# sourceMappingURL=ipc.js.map
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Housekeeper daemon lifecycle helpers (epic #3353 slice B5).
3
+ *
4
+ * Mirrors the pid/lock/heartbeat/state/socket paths of
5
+ * `tools/daemon/daemon-main.mjs` so `aiwg housekeeper start|stop|status`
6
+ * serializes against the same single-instance lock: a second `start`
7
+ * fails clearly, and `stop` cleans up exactly the files the daemon owns.
8
+ * All process signalling of owned ChildProcess handles goes through
9
+ * `signalChild`; pid-based stop uses `process.kill` (no ChildProcess).
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { signalChild } from '../process/signal-child.mjs';
14
+ /** Same relative layout as DaemonMain; resolved against the caller cwd. */
15
+ export function daemonPaths(cwd, frameworkRoot) {
16
+ return {
17
+ pidFile: path.join(cwd, '.aiwg', 'daemon.pid'),
18
+ lockFile: path.join(cwd, '.aiwg', 'daemon', '.lock'),
19
+ heartbeatFile: path.join(cwd, '.aiwg', 'daemon', 'heartbeat'),
20
+ stateFile: path.join(cwd, '.aiwg', 'daemon', 'state.json'),
21
+ socketPath: path.join(cwd, '.aiwg', 'daemon', 'daemon.sock'),
22
+ logFile: path.join(cwd, '.aiwg', 'daemon', 'daemon.log'),
23
+ taskStorePath: path.join(cwd, '.aiwg', 'daemon', 'tasks.json'),
24
+ daemonScript: path.join(frameworkRoot, 'tools', 'daemon', 'daemon-main.mjs'),
25
+ };
26
+ }
27
+ export function readPidFile(pidFile) {
28
+ let raw;
29
+ try {
30
+ raw = fs.readFileSync(pidFile, 'utf8').trim();
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ const pid = Number.parseInt(raw, 10);
36
+ return Number.isSafeInteger(pid) && pid > 0 && String(pid) === raw ? pid : null;
37
+ }
38
+ export function defaultKill(pid, signal) {
39
+ process.kill(pid, signal);
40
+ }
41
+ export function isPidAlive(pid) {
42
+ try {
43
+ process.kill(pid, 0);
44
+ return true;
45
+ }
46
+ catch (error) {
47
+ // ESRCH = no such process. Any other error (e.g. EPERM) means a
48
+ // process exists that we may not signal — treat as alive.
49
+ if (error?.code === 'ESRCH')
50
+ return false;
51
+ return true;
52
+ }
53
+ }
54
+ /**
55
+ * Report whether a daemon owns the pid file. A pid file pointing at a
56
+ * dead process is stale (safe to overwrite after cleanup); a live one
57
+ * blocks `start`.
58
+ */
59
+ export function describeRunning(pidFile) {
60
+ const pid = readPidFile(pidFile);
61
+ if (pid === null)
62
+ return { running: false, pid: null, stale: false };
63
+ if (isPidAlive(pid))
64
+ return { running: true, pid, stale: false };
65
+ return { running: false, pid, stale: true };
66
+ }
67
+ export function removeLifecycleFiles(paths) {
68
+ for (const file of [paths.pidFile, paths.lockFile]) {
69
+ try {
70
+ fs.unlinkSync(file);
71
+ }
72
+ catch (error) {
73
+ if (error?.code !== 'ENOENT')
74
+ throw error;
75
+ }
76
+ }
77
+ }
78
+ const defaultSleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
79
+ /**
80
+ * Stop the daemon gracefully (SIGTERM, poll, then SIGKILL on timeout),
81
+ * then release the pid/lock files. Never throws for absent files.
82
+ */
83
+ export async function stopDaemon(options) {
84
+ const { paths, kill = defaultKill, timeoutMs = 10_000, pollMs = 200 } = options;
85
+ const sleep = options.sleep ?? defaultSleep;
86
+ const state = describeRunning(paths.pidFile);
87
+ if (state.pid === null)
88
+ return { outcome: 'not-running' };
89
+ if (!state.running) {
90
+ removeLifecycleFiles(paths);
91
+ return { outcome: 'stale-cleaned', pid: state.pid };
92
+ }
93
+ const pid = state.pid;
94
+ try {
95
+ kill(pid, 'SIGTERM');
96
+ }
97
+ catch (error) {
98
+ if (error?.code === 'ESRCH') {
99
+ removeLifecycleFiles(paths);
100
+ return { outcome: 'stale-cleaned', pid };
101
+ }
102
+ throw error;
103
+ }
104
+ const deadline = Date.now() + timeoutMs;
105
+ while (isPidAlive(pid)) {
106
+ if (Date.now() >= deadline)
107
+ break;
108
+ await sleep(pollMs);
109
+ }
110
+ let forced = false;
111
+ if (isPidAlive(pid)) {
112
+ try {
113
+ kill(pid, 'SIGKILL');
114
+ }
115
+ catch (error) {
116
+ if (error?.code !== 'ESRCH')
117
+ throw error;
118
+ }
119
+ forced = true;
120
+ const killDeadline = Date.now() + Math.min(timeoutMs, 5000);
121
+ while (isPidAlive(pid) && Date.now() < killDeadline) {
122
+ await sleep(pollMs);
123
+ }
124
+ }
125
+ removeLifecycleFiles(paths);
126
+ return { outcome: 'stopped', pid, forced };
127
+ }
128
+ /** Forward a terminal signal to an owned foreground child. */
129
+ export function forwardSignalToChild(child, signal) {
130
+ signalChild(child, signal);
131
+ }
132
+ /**
133
+ * Poll until `predicate` is true or the deadline passes. Returns true on
134
+ * success; used by `start` to wait for the pid file without fixed sleeps.
135
+ */
136
+ export async function pollUntil(predicate, options = {}) {
137
+ const { timeoutMs = 10_000, pollMs = 200 } = options;
138
+ const sleep = options.sleep ?? defaultSleep;
139
+ const deadline = Date.now() + timeoutMs;
140
+ for (;;) {
141
+ if (predicate())
142
+ return true;
143
+ if (Date.now() >= deadline)
144
+ return false;
145
+ await sleep(pollMs);
146
+ }
147
+ }
148
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Platform honesty for `aiwg housekeeper` (epic #3353 slice B5).
3
+ *
4
+ * Design-note §3 matrix in one place, with the platform injectable so
5
+ * tests can cover every row without running on that OS: Windows is
6
+ * unsupported (WSL2 follows the Linux rows); macOS install is a
7
+ * best-effort launchd plist (documented, not CI-tested); Linux install
8
+ * targets systemd user units.
9
+ */
10
+ export function normalizePlatform(platform = process.platform) {
11
+ if (platform === 'win32')
12
+ return 'windows';
13
+ if (platform === 'darwin')
14
+ return 'darwin';
15
+ if (platform === 'linux')
16
+ return 'linux';
17
+ return 'other';
18
+ }
19
+ /** run-once / daemon processes: everything except Windows. */
20
+ export function runSupported(platform = process.platform) {
21
+ return normalizePlatform(platform) !== 'windows';
22
+ }
23
+ export function runUnsupportedMessage() {
24
+ return 'aiwg housekeeper is not supported on Windows (no recursive fs.watch, POSIX paths assumed). Use WSL2 and follow the Linux rows.';
25
+ }
26
+ /** Install backends offered on a platform, in preference order. */
27
+ export function installBackends(platform = process.platform) {
28
+ const normalized = normalizePlatform(platform);
29
+ if (normalized === 'windows')
30
+ return [];
31
+ if (normalized === 'darwin')
32
+ return ['launchd', 'systemd-user'];
33
+ return ['systemd-user', 'launchd'];
34
+ }
35
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Housekeeper status envelope (epic #3353 slice B5).
3
+ *
4
+ * `aiwg housekeeper status --json` emits this shape: the B1 §6 contract
5
+ * fields (`contractVersion`/`command`/`status`/`mode`/`jobs`/`totals`) plus
6
+ * per-index freshness measured by the B4 adapters. The CLI builds it from
7
+ * live daemon IPC when a daemon answers, otherwise from local config and
8
+ * persisted receipts (offline mode), so ordinary `aiwg` commands keep
9
+ * working with no daemon running.
10
+ */
11
+ export const HOUSEKEEPER_STATUS_CONTRACT_VERSION = 1;
12
+ const RECEIPT_STATES = new Set(['ok', 'failed', 'skipped', 'deferred']);
13
+ export function countJobTotals(jobs) {
14
+ const totals = {
15
+ jobs: jobs.length, ok: 0, failed: 0, skipped: 0, deferred: 0, never: 0,
16
+ };
17
+ for (const job of jobs) {
18
+ if (job.lastState === 'ok')
19
+ totals.ok += 1;
20
+ else if (job.lastState === 'failed')
21
+ totals.failed += 1;
22
+ else if (job.lastState === 'skipped')
23
+ totals.skipped += 1;
24
+ else if (job.lastState === 'deferred')
25
+ totals.deferred += 1;
26
+ else
27
+ totals.never += 1;
28
+ }
29
+ return totals;
30
+ }
31
+ export function normalizeJobState(state) {
32
+ return typeof state === 'string' && RECEIPT_STATES.has(state)
33
+ ? state
34
+ : 'never';
35
+ }
36
+ /** Assemble the envelope; every field is always present. */
37
+ export function buildHousekeeperStatus(input) {
38
+ const jobs = input.jobs ?? [];
39
+ return {
40
+ contractVersion: HOUSEKEEPER_STATUS_CONTRACT_VERSION,
41
+ command: 'housekeeper.status',
42
+ status: 'ok',
43
+ mode: input.mode,
44
+ pid: input.pid ?? null,
45
+ uptimeSeconds: input.uptimeSeconds ?? 0,
46
+ jobs,
47
+ totals: countJobTotals(jobs),
48
+ freshness: input.freshness ?? [],
49
+ update: {
50
+ channel: 'stable',
51
+ checkOnly: true,
52
+ lastCheck: input.lastCheck ?? null,
53
+ available: input.available ?? null,
54
+ lastInstall: input.lastInstall ?? null,
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * Map a B4 freshness snapshot to the status entry. `unavailable` (no local
60
+ * deterministic probe) surfaces as `unsupported` per the B1 §6 state set.
61
+ */
62
+ export function toFreshnessEntry(snapshot) {
63
+ return {
64
+ index: snapshot.name,
65
+ state: snapshot.status === 'unavailable' ? 'unsupported' : snapshot.status,
66
+ targetMs: snapshot.policy?.targetStaleAfterMs ?? null,
67
+ converged: snapshot.convergence?.converged ?? null,
68
+ };
69
+ }
70
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Autostart unit renderers for `aiwg housekeeper install` (epic #3353 B5).
3
+ *
4
+ * Platform matrix (design note §3): Linux + systemd is documented;
5
+ * Linux without systemd and macOS fall back to foreground `start` or an
6
+ * external timer calling `run-once`; Windows is unsupported (WSL2 follows
7
+ * the Linux rows). Units are pure strings here so `--dry-run` can print
8
+ * them without writing and tests can assert content with an injected
9
+ * platform. This module never enables anything: enabling is an explicit
10
+ * operator step printed by the handler, never performed implicitly.
11
+ */
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ export const HOUSEKEEPER_SERVICE_NAME = 'aiwg-housekeeper.service';
15
+ export const HOUSEKEEPER_TIMER_NAME = 'aiwg-housekeeper.timer';
16
+ export const HOUSEKEEPER_LAUNCHD_LABEL = 'dev.aiwg.housekeeper';
17
+ export function renderSystemdService(ctx) {
18
+ const node = ctx.nodeBin ?? process.execPath;
19
+ const runOnce = ctx.jobId
20
+ ? `${quote(ctx.aiwgBin)} housekeeper run-once --job ${quote(ctx.jobId)}`
21
+ : `${quote(ctx.aiwgBin)} housekeeper run-once`;
22
+ return [
23
+ '[Unit]',
24
+ 'Description=AIWG housekeeper (deterministic maintenance, oneshot for timer)',
25
+ 'Documentation=https://github.com/jmagly/aiwg',
26
+ '',
27
+ '[Service]',
28
+ 'Type=oneshot',
29
+ `WorkingDirectory=${ctx.cwd}`,
30
+ `ExecStart=${node} ${runOnce}`,
31
+ 'NoNewPrivileges=true',
32
+ '',
33
+ '[Install]',
34
+ 'WantedBy=default.target',
35
+ '',
36
+ ].join('\n');
37
+ }
38
+ export function renderSystemdTimer(options = {}) {
39
+ return [
40
+ '[Unit]',
41
+ 'Description=AIWG housekeeper periodic run (calls aiwg housekeeper run-once)',
42
+ '',
43
+ '[Timer]',
44
+ `OnCalendar=${options.calendar ?? 'hourly'}`,
45
+ 'Persistent=true',
46
+ `Unit=${HOUSEKEEPER_SERVICE_NAME}`,
47
+ '',
48
+ '[Install]',
49
+ 'WantedBy=timers.target',
50
+ '',
51
+ ].join('\n');
52
+ }
53
+ /**
54
+ * Best-effort macOS plist (design note §3: documented, not CI-tested —
55
+ * there is no macOS runner). Runs `run-once` on a calendar interval.
56
+ */
57
+ export function renderLaunchdPlist(ctx, options = {}) {
58
+ const node = ctx.nodeBin ?? process.execPath;
59
+ const args = ctx.jobId
60
+ ? [ctx.aiwgBin, 'housekeeper', 'run-once', '--job', ctx.jobId]
61
+ : [ctx.aiwgBin, 'housekeeper', 'run-once'];
62
+ const argsXml = [node, ...args].map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n');
63
+ return [
64
+ '<?xml version="1.0" encoding="UTF-8"?>',
65
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
66
+ '<plist version="1.0">',
67
+ '<dict>',
68
+ ' <key>Label</key>',
69
+ ` <string>${HOUSEKEEPER_LAUNCHD_LABEL}</string>`,
70
+ ' <key>ProgramArguments</key>',
71
+ ' <array>',
72
+ argsXml,
73
+ ' </array>',
74
+ ' <key>WorkingDirectory</key>',
75
+ ` <string>${xmlEscape(ctx.cwd)}</string>`,
76
+ ' <key>StartInterval</key>',
77
+ ` <integer>${options.intervalSeconds ?? 3600}</integer>`,
78
+ ' <key>RunAtLoad</key>',
79
+ ' <false/>',
80
+ '</dict>',
81
+ '</plist>',
82
+ '',
83
+ ].join('\n');
84
+ }
85
+ /** Default `aiwg` entry point under a framework root (overridable in tests). */
86
+ export function defaultAiwgBin(frameworkRoot) {
87
+ return path.join(frameworkRoot, 'bin', 'aiwg.mjs');
88
+ }
89
+ /** XDG user systemd directory (`~/.config/systemd/user` by default). */
90
+ export function systemdUserDir(home = os.homedir()) {
91
+ const base = process.env['XDG_CONFIG_HOME'] || path.join(home, '.config');
92
+ return path.join(base, 'systemd', 'user');
93
+ }
94
+ /** macOS user agent directory. */
95
+ export function launchdAgentDir(home = os.homedir()) {
96
+ return path.join(home, 'Library', 'LaunchAgents');
97
+ }
98
+ export function backendForPlatform(platform) {
99
+ if (platform === 'win32')
100
+ return 'unsupported';
101
+ if (platform === 'darwin')
102
+ return 'launchd';
103
+ return 'systemd-user';
104
+ }
105
+ function quote(value) {
106
+ return /[\s"']/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
107
+ }
108
+ function xmlEscape(value) {
109
+ return value
110
+ .replace(/&/g, '&amp;')
111
+ .replace(/</g, '&lt;')
112
+ .replace(/>/g, '&gt;');
113
+ }
114
+ //# sourceMappingURL=units.js.map
@@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { load as loadYaml } from 'js-yaml';
4
4
  import { z } from 'zod';
5
+ import { integer, url } from '../schema/zod-validation.mjs';
5
6
  import { JOB_API_VERSION, JOB_KIND } from './types.js';
6
7
  const relativeFile = z.string().min(1).refine(value => {
7
8
  if (path.isAbsolute(value))
@@ -12,51 +13,51 @@ const relativeFile = z.string().min(1).refine(value => {
12
13
  const absolutePath = z.string().min(1)
13
14
  .refine(path.isAbsolute, 'must be an absolute path')
14
15
  .refine(value => path.resolve(value) !== path.parse(path.resolve(value)).root, 'must not be a filesystem root');
15
- const httpsOrigin = z.string().url().refine(value => new URL(value).protocol === 'https:', 'must use https');
16
+ const httpsOrigin = url().refine(value => new URL(value).protocol === 'https:', 'must use https');
16
17
  const accountName = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.@:-]{0,127}$/u);
17
- export const externalJobFlowSchema = z.object({
18
+ export const externalJobFlowSchema = z.strictObject({
18
19
  apiVersion: z.literal(JOB_API_VERSION),
19
20
  kind: z.literal(JOB_KIND),
20
- metadata: z.object({
21
+ metadata: z.strictObject({
21
22
  name: z.string().regex(/^[a-z][a-z0-9-]{0,62}$/u),
22
23
  revision: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u),
23
- }).strict(),
24
- spec: z.object({
25
- trigger: z.object({ type: z.literal('external') }).strict(),
26
- executor: z.object({
24
+ }),
25
+ spec: z.strictObject({
26
+ trigger: z.strictObject({ type: z.literal('external') }),
27
+ executor: z.strictObject({
27
28
  provider: z.literal('codex'),
28
29
  mode: z.literal('exec'),
29
30
  workspace: absolutePath,
30
31
  prompt: relativeFile,
31
32
  resultSchema: relativeFile,
32
33
  binary: absolutePath,
33
- }).strict(),
34
- workItem: z.object({
34
+ }),
35
+ workItem: z.strictObject({
35
36
  provider: z.literal('gitea'),
36
- baseUrl: z.string().url().refine(value => new URL(value).protocol === 'https:', 'must use https'),
37
+ baseUrl: url().refine(value => new URL(value).protocol === 'https:', 'must use https'),
37
38
  repository: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u),
38
39
  tokenFile: absolutePath,
39
40
  eligibleLabels: z.array(z.string().min(1)).min(1),
40
- claimTtlSeconds: z.number().int().min(30).max(86400).optional(),
41
- claimSettleMs: z.number().int().min(100).max(30000).optional(),
42
- }).strict(),
43
- approval: z.object({
41
+ claimTtlSeconds: integer().min(30).max(86400).optional(),
42
+ claimSettleMs: integer().min(100).max(30000).optional(),
43
+ }),
44
+ approval: z.strictObject({
44
45
  required: z.boolean().optional(),
45
46
  label: z.string().min(1).optional(),
46
- }).strict().optional(),
47
- security: z.object({
47
+ }).optional(),
48
+ security: z.strictObject({
48
49
  allowedOrigins: z.array(httpsOrigin).min(1),
49
50
  allowedAccounts: z.array(accountName).min(1),
50
51
  approvedAttachmentRoots: z.array(absolutePath),
51
52
  sensitiveValueFiles: z.array(absolutePath).optional(),
52
- }).strict(),
53
- completion: z.object({
53
+ }),
54
+ completion: z.strictObject({
54
55
  require: z.array(z.enum([
55
56
  'external-result-url', 'issue-comment', 'idempotency-key', 'verification',
56
57
  ])).min(1),
57
- }).strict(),
58
- }).strict(),
59
- }).strict().superRefine((flow, ctx) => {
58
+ }),
59
+ }),
60
+ }).superRefine((flow, ctx) => {
60
61
  const workspace = path.resolve(flow.spec.executor.workspace);
61
62
  const within = (candidate) => {
62
63
  const relative = path.relative(workspace, path.resolve(candidate));
@@ -1,5 +1,8 @@
1
+ /** Activation checks within the existing lint runner, not a delivery selector. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
1
4
  import { loadLocalCapabilityEntries, resolveRuleBindingTarget } from '../artifacts/query-engine.js';
2
- import { parseRuleFrontmatter, RULE_BINDING_TYPES } from '../rules/activation.mjs';
5
+ import { parseRuleFrontmatter, RULE_BINDING_TYPES, coreBodyPath, validateCoreBody } from '../rules/activation.mjs';
3
6
  export const ruleActivationRuleset = {
4
7
  id: 'rule-activation', name: 'Rule activation', framework: 'core', version: '1.0.0',
5
8
  description: 'Validate rule and behavior activation contracts and exact indexed bindings',
@@ -10,17 +13,29 @@ export const ruleActivationRuleset = {
10
13
  checks: [{ type: 'rule-activation' }],
11
14
  }],
12
15
  };
13
- export function lintRuleActivation(content, file, projectRoot, entries) {
14
- // Generated structural indexes are not authored rules.
15
- if (/^(?:RULES-INDEX|RULES-ONDEMAND)\.md$/.test(file.split('/').pop()))
16
+ export function lintRuleActivation(content, file, projectRoot, entries, absolutePath) {
17
+ // Directory documentation and generated indexes are not authored rules.
18
+ if (/^(?:README|RULES-INDEX|RULES-ONDEMAND)\.md$/.test(file.split('/').pop()))
16
19
  return [];
20
+ // <rule>.core.md is a delivery body (#3450), validated through its owning rule.
21
+ if (/\.core\.md$/.test(file))
22
+ return [];
23
+ const shipped = /(?:^|\/)agentic\/code\//.test((absolutePath ?? path.resolve(projectRoot, file)).replaceAll('\\', '/'));
17
24
  const result = parseRuleFrontmatter(content);
18
25
  const diagnostics = result.diagnostics.map(item => ({
19
26
  ruleId: 'rules/activation', ruleName: 'Rule activation contract',
20
- file, severity: item.severity, message: item.message,
27
+ file, severity: item.code === 'missing-activation' && shipped ? 'error' : item.severity, message: item.message,
21
28
  }));
22
29
  if (!result.rule)
23
30
  return diagnostics;
31
+ if (result.rule.activation === 'core') {
32
+ const body = coreBodyPath(absolutePath ?? (path.isAbsolute(file) ? file : path.resolve(projectRoot, file)));
33
+ const problems = fs.existsSync(body)
34
+ ? validateCoreBody(fs.readFileSync(body, 'utf8').trim(), path.basename(body))
35
+ : [`${path.basename(body)}: activation core requires a compact body file beside the rule`];
36
+ for (const message of problems)
37
+ diagnostics.push({ ruleId: 'rules/activation', ruleName: 'Rule activation contract', file, severity: 'error', message });
38
+ }
24
39
  const bindings = result.rule.bound_to;
25
40
  if (!Object.values(bindings).some(list => list.length > 0))
26
41
  return diagnostics;
@@ -563,7 +563,7 @@ async function runRule(rule, targetDir, allFiles, referenceIndex, projectRoot, a
563
563
  if (check.type === 'id-unique')
564
564
  continue; // Already handled
565
565
  if (check.type === 'rule-activation') {
566
- diagnostics.push(...lintRuleActivation(content, file, projectRoot, artifactEntries));
566
+ diagnostics.push(...lintRuleActivation(content, file, projectRoot, artifactEntries, absPath));
567
567
  continue;
568
568
  }
569
569
  const checkDiagnostics = runCheck(check, content, frontmatter, file, targetDir, allFiles, referenceIndex, projectRoot);
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { startServer, createServer } from './server.mjs';
10
10
  import { assertConfigDestination, assertConfigObject, assertProjectCredentials, isPathInsideDir, classifyMcpDestination, projectCredentialRemedy, writeConfigAtomic, writeConfigTransaction } from './config-file.mjs';
11
+ import { replaceServer } from './toml-editor.mjs';
11
12
  import fs from 'fs/promises';
12
13
  import path from 'path';
13
14
  import os from 'os';
@@ -335,16 +336,14 @@ enabled_tools = [
335
336
  if (error.code !== 'ENOENT') throw error;
336
337
  }
337
338
 
338
- // Check if AIWG MCP already configured
339
- if (existing.includes('[mcp_servers.aiwg]')) {
339
+ // Parse before deciding whether to write, including already configured files.
340
+ const edited = replaceServer(existing, 'aiwg', tomlContent.trim());
341
+ if (edited.alreadyPresent) {
340
342
  console.log('AIWG MCP already configured in ~/.codex/config.toml');
341
343
  return true;
342
344
  }
343
345
 
344
- // Append TOML config
345
- const updated = existing.trimEnd() + '\n' + tomlContent.trim() + '\n';
346
-
347
- await writeConfigAtomic(configPath, updated, { userScope: true });
346
+ await writeConfigAtomic(configPath, edited.text, { userScope: true });
348
347
  console.log(`MCP configuration appended to: ${configPath}`);
349
348
  console.log(`\nTo use AIWG MCP server with Codex:`);
350
349
  console.log(` 1. Restart Codex CLI`);
@@ -187,11 +187,15 @@ export async function writeConfigAtomic(file, content, { userScope = false, proj
187
187
  if (rejectSymlinkParents) await assertNoSymlinkParents(file, symlinkRoot);
188
188
  const info = await assertConfigDestination(file, projectRoot);
189
189
  const mode = restoreMode ?? (userScope ? 0o600 : info ? info.mode & 0o7777 : newFileMode ?? 0o666);
190
- await mkdir(dirname(file), { recursive: true });
190
+ // Restrict newly created parents for private settings, ownership and output files.
191
+ // Recursive mkdir leaves every existing directory's permissions unchanged.
192
+ await mkdir(dirname(file), { recursive: true, mode: (mode & 0o077) === 0 ? 0o700 : undefined });
191
193
  const temporary = `${file}.${randomUUID()}.tmp`;
192
194
  let handle;
195
+ let created = false;
193
196
  try {
194
197
  handle = await open(temporary, 'wx', mode);
198
+ created = true;
195
199
  await handle.writeFile(content, 'utf-8');
196
200
  if (userScope || info || restoreMode !== undefined || newFileMode !== undefined) await handle.chmod(mode);
197
201
  await handle.sync();
@@ -204,7 +208,7 @@ export async function writeConfigAtomic(file, content, { userScope = false, proj
204
208
  try {
205
209
  if (handle) await handle.close();
206
210
  } finally {
207
- await unlink(temporary).catch(() => {});
211
+ if (created) await unlink(temporary).catch(() => {});
208
212
  }
209
213
  }
210
214
  }