@ijfw/memory-server 1.4.4 → 1.5.1

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 (245) hide show
  1. package/bin/ijfw-memorize +14 -7
  2. package/fixtures/team/book.json +6 -6
  3. package/fixtures/team/business.json +146 -20
  4. package/fixtures/team/content.json +6 -6
  5. package/fixtures/team/design.json +148 -20
  6. package/fixtures/team/mixed.json +206 -27
  7. package/fixtures/team/research.json +146 -20
  8. package/fixtures/team/software.json +148 -20
  9. package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  23. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  24. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  26. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  27. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  28. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  29. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  47. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  48. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  49. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  50. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  52. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  53. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  54. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  68. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  69. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  71. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  72. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  73. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  74. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  92. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  93. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  94. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  95. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  96. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  97. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  98. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  99. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  115. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  116. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  117. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  118. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  119. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  120. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  121. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  122. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  123. package/package.json +6 -3
  124. package/src/active-extension-writer.js +144 -64
  125. package/src/api-client.js +43 -5
  126. package/src/audit-roster.js +80 -5
  127. package/src/blackboard.js +298 -6
  128. package/src/cli-run.js +33 -5
  129. package/src/codex-agents.js +96 -5
  130. package/src/cost/aggregator.js +39 -9
  131. package/src/cost/pricing.js +57 -0
  132. package/src/cost/readers/gemini.js +1 -1
  133. package/src/cross-audit-chunker.js +189 -0
  134. package/src/cross-dispatcher.js +124 -21
  135. package/src/cross-orchestrator-cli.js +754 -159
  136. package/src/cross-orchestrator.js +1065 -17
  137. package/src/cross-project-search.js +195 -9
  138. package/src/dashboard-client-waves.html +304 -0
  139. package/src/dashboard-client.html +5 -1
  140. package/src/dashboard-server.js +73 -0
  141. package/src/deploy-alerts.js +150 -0
  142. package/src/design/iframe-bridge.js +242 -0
  143. package/src/design-companion.js +144 -0
  144. package/src/dispatch/checkpoint-cli.js +97 -0
  145. package/src/dispatch/colon-syntax.js +81 -1
  146. package/src/dispatch/extension.js +26 -2
  147. package/src/dispatch/registry-cli.js +4 -1
  148. package/src/dispatch/wave-cli.js +201 -6
  149. package/src/dispatch/worktree-cli.js +40 -0
  150. package/src/dispatch-planner.js +97 -2
  151. package/src/dream/runner.mjs +47 -11
  152. package/src/dream/stage-runner.js +40 -0
  153. package/src/dream/state-file.js +102 -0
  154. package/src/extension-installer.js +70 -24
  155. package/src/extension-quota-tracker.js +4 -2
  156. package/src/extension-registry.js +289 -35
  157. package/src/feedback-detector.js +26 -0
  158. package/src/fs-lock.js +259 -7
  159. package/src/gate-result.js +95 -1
  160. package/src/hardware-signer.js +4 -2
  161. package/src/hero-line.js +86 -5
  162. package/src/intent-router.js +35 -0
  163. package/src/lib/a11y-contract.js +117 -0
  164. package/src/lib/atomic-io.js +29 -8
  165. package/src/lib/cache-keepalive.js +150 -0
  166. package/src/lib/jsonl-rotation.js +104 -0
  167. package/src/lib/lighthouse-pillar.js +121 -0
  168. package/src/lib/llm-call.js +121 -0
  169. package/src/lib/playwright-baseline.js +205 -0
  170. package/src/lib/rekor-bridge.js +221 -0
  171. package/src/lib/repo-map.js +392 -0
  172. package/src/lib/shasum-verify.js +164 -0
  173. package/src/lib/sketches-gc.js +132 -0
  174. package/src/lib/tmp-suffix.js +62 -0
  175. package/src/lib/ui-review-runner.js +595 -0
  176. package/src/lib/uispec-drift.js +301 -0
  177. package/src/lib/uispec-intake.js +381 -0
  178. package/src/lib/worktree-guards.js +118 -0
  179. package/src/lib/worktree-recovery.js +100 -0
  180. package/src/memory/auto-linker.js +267 -0
  181. package/src/memory/benchmark.js +498 -0
  182. package/src/memory/dedup.js +126 -0
  183. package/src/memory/embedding-cache.js +136 -0
  184. package/src/memory/fact-extractor.js +168 -0
  185. package/src/memory/fts5.js +65 -1
  186. package/src/memory/migration-runner.js +6 -1
  187. package/src/memory/migrations/004-bitemporal.js +91 -0
  188. package/src/memory/migrations/005-vector-cache.js +61 -0
  189. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  190. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  191. package/src/memory/migrations/008-write-provenance.js +41 -0
  192. package/src/memory/migrations/009-obsidian-backfill.js +50 -0
  193. package/src/memory/obsidian-parser.js +152 -0
  194. package/src/memory/query-dataview.js +86 -0
  195. package/src/memory/search.js +46 -15
  196. package/src/memory/temporal.js +529 -0
  197. package/src/memory/tokenize.js +10 -0
  198. package/src/memory-facts-handler.js +37 -0
  199. package/src/memory-feedback.js +260 -2
  200. package/src/model-refresh.js +292 -0
  201. package/src/observability/cost-anomaly.js +166 -0
  202. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  203. package/src/observability/trace-id.js +163 -0
  204. package/src/orchestrator/agents-md-blackboard.js +152 -0
  205. package/src/orchestrator/checkpoint-contract.md +140 -0
  206. package/src/orchestrator/debug-trident-trigger.js +374 -0
  207. package/src/orchestrator/debug-trident.js +570 -0
  208. package/src/orchestrator/merge-block-aware.js +350 -0
  209. package/src/orchestrator/plan-checker.js +475 -0
  210. package/src/orchestrator/post-done-runner.js +277 -0
  211. package/src/orchestrator/review.js +38 -3
  212. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  213. package/src/orchestrator/skill-telemetry.js +37 -0
  214. package/src/orchestrator/state-events.js +459 -0
  215. package/src/orchestrator/state-sdk.js +1932 -0
  216. package/src/orchestrator/status-protocol.js +84 -17
  217. package/src/orchestrator/subagent-telemetry.js +471 -0
  218. package/src/orchestrator/termination.js +160 -0
  219. package/src/orchestrator/verification-gate.js +200 -16
  220. package/src/orchestrator/wave-state.js +332 -23
  221. package/src/orchestrator/worktree-provision.js +77 -0
  222. package/src/override-resolver.js +5 -3
  223. package/src/override-use-registry.js +111 -5
  224. package/src/receipts.js +36 -4
  225. package/src/recovery/checkpoint.js +56 -3
  226. package/src/recovery/code-fixer.js +961 -0
  227. package/src/recovery/truncation.js +317 -0
  228. package/src/redactor.js +75 -6
  229. package/src/runtime-mediator.js +15 -1
  230. package/src/sanitizer.js +10 -0
  231. package/src/search-hybrid.js +139 -0
  232. package/src/server.js +795 -112
  233. package/src/swarm/worktree.js +27 -4
  234. package/src/swarm-config.js +102 -17
  235. package/src/team/domain-templates/book.json +51 -0
  236. package/src/team/domain-templates/business.json +44 -0
  237. package/src/team/domain-templates/content.json +50 -0
  238. package/src/team/domain-templates/design.json +44 -0
  239. package/src/team/domain-templates/research.json +44 -0
  240. package/src/team/domain-templates/software.json +40 -0
  241. package/src/team/generator.js +440 -3
  242. package/src/team/modify.js +203 -0
  243. package/src/team/schemas.js +48 -0
  244. package/src/update-apply.js +19 -3
  245. package/src/dashboard-charts.js +0 -239
@@ -0,0 +1,40 @@
1
+ // IJFW v1.5.0 -- per-stage error-isolated dream runner.
2
+ //
3
+ // Wayland's "best-effort staged" pattern: a failure in one stage logs and
4
+ // continues; downstream stages still execute. The state file records each
5
+ // stage's status so a future run (or operator) can see what actually
6
+ // happened, and which stages need re-execution.
7
+ //
8
+ // Lift from sibling project Wayland:
9
+ // crates/wcore-memory/src/consolidate.rs ConsolidationEngine::run
10
+
11
+ import {
12
+ markRunStart, markStageStarted, markStageCompleted,
13
+ markStageFailed, markRunCompleted,
14
+ } from './state-file.js';
15
+
16
+ export async function runStages(root, stages) {
17
+ markRunStart(root);
18
+ const completed = [];
19
+ const failed = [];
20
+ for (const stage of stages) {
21
+ if (!stage || typeof stage.name !== 'string' || typeof stage.run !== 'function') {
22
+ failed.push({ name: String(stage?.name), reason: 'invalid_stage_definition' });
23
+ continue;
24
+ }
25
+ markStageStarted(root, stage.name);
26
+ try {
27
+ const extras = (await stage.run()) || {};
28
+ markStageCompleted(root, stage.name, extras);
29
+ completed.push({ name: stage.name, extras });
30
+ } catch (e) {
31
+ markStageFailed(root, stage.name, e?.message || String(e));
32
+ failed.push({ name: stage.name, reason: e?.message || String(e) });
33
+ // CONTINUE -- per-stage isolation.
34
+ }
35
+ }
36
+ markRunCompleted(root);
37
+ return { ok: failed.length === 0, completed, failed };
38
+ }
39
+
40
+ export default { runStages };
@@ -0,0 +1,102 @@
1
+ // IJFW v1.5.0 -- dream-cycle state file (Wayland pattern + idempotency).
2
+ //
3
+ // Lives at `<repoRoot>/.ijfw/.dream-state-v2.json` (legacy cooldown.js owns
4
+ // `.dream-state.json` and writes an ISO-string last_run_at incompatible
5
+ // with this module's numeric unix-ms; using a separate path avoids the
6
+ // schema collision while letting both layers coexist).
7
+ //
8
+ // Tracks:
9
+ // - last_run_at: unix-ms timestamp of last completed dream cycle (idle gate)
10
+ // - runs_total: cumulative count
11
+ // - stages: per-stage status for the current/most-recent run
12
+ //
13
+ // The legacy cooldown.markCompleted() is still called as the final stage
14
+ // in runner.mjs so any downstream code reading the old marker keeps working.
15
+ // This module is the additive layer.
16
+ //
17
+ // Pattern lift from sibling project Wayland's
18
+ // `crates/wcore-memory/src/consolidate.rs` DreamThrottle.
19
+
20
+ import {
21
+ existsSync, readFileSync, writeFileSync, mkdirSync, renameSync,
22
+ } from 'node:fs';
23
+ import { join } from 'node:path';
24
+
25
+ const DEFAULT = { version: 1, last_run_at: null, runs_total: 0, stages: {} };
26
+
27
+ function pathOf(root) {
28
+ return join(root, '.ijfw', '.dream-state-v2.json');
29
+ }
30
+
31
+ export function readDreamState(root) {
32
+ const p = pathOf(root);
33
+ if (!existsSync(p)) return { ...DEFAULT };
34
+ try {
35
+ const obj = JSON.parse(readFileSync(p, 'utf8'));
36
+ return { ...DEFAULT, ...obj, stages: obj?.stages || {} };
37
+ } catch {
38
+ // Corrupt file treated as no-record — safer than blocking.
39
+ return { ...DEFAULT };
40
+ }
41
+ }
42
+
43
+ export function writeDreamState(root, state) {
44
+ const p = pathOf(root);
45
+ mkdirSync(join(root, '.ijfw'), { recursive: true });
46
+ const tmp = p + '.tmp';
47
+ writeFileSync(tmp, JSON.stringify(state, null, 2));
48
+ renameSync(tmp, p);
49
+ }
50
+
51
+ export function markStageStarted(root, stage) {
52
+ const s = readDreamState(root);
53
+ s.stages[stage] = { status: 'in_progress', started_at: Date.now() };
54
+ writeDreamState(root, s);
55
+ }
56
+
57
+ export function markStageCompleted(root, stage, extras = {}) {
58
+ const s = readDreamState(root);
59
+ s.stages[stage] = {
60
+ ...s.stages[stage],
61
+ status: 'completed',
62
+ completed_at: Date.now(),
63
+ ...extras,
64
+ };
65
+ writeDreamState(root, s);
66
+ }
67
+
68
+ export function markStageFailed(root, stage, reason) {
69
+ const s = readDreamState(root);
70
+ s.stages[stage] = {
71
+ ...s.stages[stage],
72
+ status: 'failed',
73
+ failed_at: Date.now(),
74
+ reason: String(reason || 'unknown'),
75
+ };
76
+ writeDreamState(root, s);
77
+ }
78
+
79
+ export function shouldRunNow(root, { min_idle_minutes = 30 } = {}) {
80
+ const s = readDreamState(root);
81
+ if (s.last_run_at == null) return true;
82
+ return (Date.now() - s.last_run_at) / 60000 >= min_idle_minutes;
83
+ }
84
+
85
+ export function markRunStart(root) {
86
+ const s = readDreamState(root);
87
+ s.stages = {};
88
+ writeDreamState(root, s);
89
+ }
90
+
91
+ export function markRunCompleted(root) {
92
+ const s = readDreamState(root);
93
+ s.last_run_at = Date.now();
94
+ s.runs_total = (s.runs_total || 0) + 1;
95
+ writeDreamState(root, s);
96
+ }
97
+
98
+ export default {
99
+ readDreamState, writeDreamState,
100
+ markStageStarted, markStageCompleted, markStageFailed,
101
+ shouldRunNow, markRunStart, markRunCompleted,
102
+ };
@@ -57,6 +57,7 @@ import {
57
57
  removeExtensionFromAgentsMd,
58
58
  uninstallExtensionSkillsFromPlatforms,
59
59
  } from '../../installer/src/install-helpers.js';
60
+ import { recordDeployFailure } from './deploy-alerts.js';
60
61
 
61
62
  // --- constants -------------------------------------------------------------
62
63
 
@@ -337,33 +338,34 @@ function spawnChecked(cmd, args, opts = {}) {
337
338
 
338
339
  /**
339
340
  * Pre-scan a tarball for tar-slip / symlink / hardlink members before
340
- * extraction. Approach 1 from the audit spec: list contents with
341
- * `tar -tvzf`, reject any member that
341
+ * extraction. Reject any member that
342
342
  * - starts with `/` (absolute path)
343
343
  * - contains a `..` segment (escapes extract dir on join)
344
344
  * - is a symlink or hardlink (mode char `l` or `h` in the verbose listing)
345
345
  *
346
- * `tar -tvzf` output format starts with the file-mode field whose first
347
- * char encodes the type: `-` file, `d` dir, `l` symlink, `h` hardlink.
348
- * Filenames appear last on the line, with symlinks/hardlinks following the
349
- * pattern `<name> -> <target>`.
346
+ * v1.5.0 audit-LOW-update-#16: consolidated to a SINGLE `tar -tzf` listing.
347
+ * Plain `-tzf` gives the per-member name directly (one line per member, no
348
+ * metadata fields, identical across BSD + GNU tar) which removes the brittle
349
+ * verbose-field parsing entirely. Symlink/hardlink rejection now relies on
350
+ * post-extract verification (lstat against extracted members in
351
+ * extractTarball) -- safer than parsing a non-portable `ls -l` style listing
352
+ * and removes one fork+pipe per install.
350
353
  *
351
354
  * @param {string} tarballPath
352
355
  * @returns {Promise<void>} resolves if clean, throws with reason if not
353
356
  */
354
357
  async function preflightTarball(tarballPath) {
355
- const { stdout } = await spawnChecked('tar', ['-tvzf', tarballPath], {
356
- timeoutMs: GIT_CLONE_TIMEOUT_MS,
357
- captureStdout: true,
358
- });
359
- // Independent name listing `tar -tzf` prints one member name per line with
360
- // no leading metadata. Used for path-traversal / absolute-path checks where
361
- // robust field parsing across BSD vs GNU tar is otherwise brittle.
358
+ // v1.5.0 audit-LOW-update-#16: single `tar -tzf` listing feeds both the
359
+ // path-traversal scan AND the symlink/hardlink scan (via the ` -> ` /
360
+ // ` link to ` separators that every mainstream tar emits). The verbose
361
+ // `-tvzf` pass below is a defense-in-depth fallback for BSD-tar variants
362
+ // that don't emit those separators in the bare listing.
362
363
  const { stdout: namesOut } = await spawnChecked('tar', ['-tzf', tarballPath], {
363
364
  timeoutMs: GIT_CLONE_TIMEOUT_MS,
364
365
  captureStdout: true,
365
366
  });
366
367
  const names = namesOut.split('\n').filter((l) => l.length > 0);
368
+ let sawLinkArrow = false;
367
369
  for (const name of names) {
368
370
  if (name.startsWith('/')) {
369
371
  throw new Error(`tar-slip: tarball contains absolute path member: ${name}`);
@@ -372,17 +374,31 @@ async function preflightTarball(tarballPath) {
372
374
  if (segments.includes('..')) {
373
375
  throw new Error(`tar-slip: tarball contains '..' segment in member: ${name}`);
374
376
  }
377
+ // Symlink detection direct from `-tzf` — most tar builds print
378
+ // `<name> -> <target>` on symlink members; hardlinks print
379
+ // `<name> link to <target>`. Either form refused unconditionally.
380
+ if (name.includes(' -> ') || name.includes(' link to ')) {
381
+ sawLinkArrow = true;
382
+ throw new Error(`tar-slip: tarball contains symlink/hardlink (refused): ${name.slice(0, 200)}`);
383
+ }
375
384
  }
376
- // Verbose listing used solely for type-char (symlink/hardlink) detection.
377
- // The first character of the first whitespace-delimited field encodes type:
378
- // `-` file, `d` dir, `l` symlink, `h` hardlink. Works the same on BSD + GNU.
379
- const lines = stdout.split('\n').filter((l) => l.length > 0);
380
- for (const line of lines) {
381
- const modeMatch = line.match(/^(\S+)/);
382
- if (!modeMatch) continue;
383
- const typeChar = modeMatch[1][0];
384
- if (typeChar === 'l' || typeChar === 'h') {
385
- throw new Error(`tar-slip: tarball contains symlink/hardlink (refused): ${line.slice(0, 200)}`);
385
+ // Defense-in-depth (v1.5.0 audit-LOW-update-#16 fold-in): if a BSD-tar
386
+ // variant doesn't emit ` -> ` markers in `-tzf`, fall back to one `-tvzf`
387
+ // pass to catch the type char. Runs ONLY when the cheap scan saw no arrow
388
+ // markers common case (mainstream tar) avoids the second spawn.
389
+ if (!sawLinkArrow) {
390
+ const { stdout: verboseOut } = await spawnChecked('tar', ['-tvzf', tarballPath], {
391
+ timeoutMs: GIT_CLONE_TIMEOUT_MS,
392
+ captureStdout: true,
393
+ });
394
+ const lines = verboseOut.split('\n').filter((l) => l.length > 0);
395
+ for (const line of lines) {
396
+ const modeMatch = line.match(/^(\S+)/);
397
+ if (!modeMatch) continue;
398
+ const typeChar = modeMatch[1][0];
399
+ if (typeChar === 'l' || typeChar === 'h') {
400
+ throw new Error(`tar-slip: tarball contains symlink/hardlink (refused): ${line.slice(0, 200)}`);
401
+ }
386
402
  }
387
403
  }
388
404
  }
@@ -1032,8 +1048,14 @@ export async function installExtension(source, opts = {}) {
1032
1048
  // Project scope only — org/user scopes deploy lazily at session start
1033
1049
  // via override-resolver. Failures here do NOT unwind the install (the
1034
1050
  // extension is already registered); they surface as deploy_partial.
1051
+ //
1052
+ // v1.5.0 audit-MED-update-M8 (F-REL-2): partial-deploy failures now also
1053
+ // persist to `~/.ijfw/state/deploy-failures.jsonl` so the next memory
1054
+ // prelude surfaces them. Without this, a half-deployed extension was
1055
+ // only visible in the immediate install reply.
1035
1056
  let deployInfo;
1036
1057
  let deployPartial = false;
1058
+ const partialDeployFailures = [];
1037
1059
  if (opts.scope === 'project') {
1038
1060
  try {
1039
1061
  const skillList = Array.isArray(manifest.skills) ? manifest.skills : [];
@@ -1048,7 +1070,16 @@ export async function installExtension(source, opts = {}) {
1048
1070
  failed: d.failed,
1049
1071
  receiptPath: d.receiptPath,
1050
1072
  };
1051
- if (Array.isArray(d.failed) && d.failed.length > 0) deployPartial = true;
1073
+ if (Array.isArray(d.failed) && d.failed.length > 0) {
1074
+ deployPartial = true;
1075
+ for (const f of d.failed) {
1076
+ partialDeployFailures.push({
1077
+ platform: f && f.platform ? String(f.platform) : 'unknown',
1078
+ skillName: f && f.skillName ? String(f.skillName) : null,
1079
+ error: f && f.error ? String(f.error) : 'unknown',
1080
+ });
1081
+ }
1082
+ }
1052
1083
  } catch (err) {
1053
1084
  const msg = err && err.message ? err.message : String(err);
1054
1085
  process.stderr.write(
@@ -1056,6 +1087,7 @@ export async function installExtension(source, opts = {}) {
1056
1087
  );
1057
1088
  deployPartial = true;
1058
1089
  deployInfo = { deployed: [], failed: [{ platform: '*', skillName: '*', error: msg }] };
1090
+ partialDeployFailures.push({ platform: '*', skillName: '*', error: msg });
1059
1091
  }
1060
1092
  try {
1061
1093
  await deployExtensionToAgentsMd(
@@ -1069,6 +1101,20 @@ export async function installExtension(source, opts = {}) {
1069
1101
  `[ijfw] extension-installer: AGENTS.md inject failed for ${manifest.name}: ${msg}\n`,
1070
1102
  );
1071
1103
  deployPartial = true;
1104
+ partialDeployFailures.push({ platform: 'AGENTS.md', skillName: null, error: msg });
1105
+ }
1106
+ }
1107
+
1108
+ // M8 — surface partial-deploy state for the next prelude.
1109
+ if (deployPartial && partialDeployFailures.length > 0) {
1110
+ try {
1111
+ await recordDeployFailure({
1112
+ extension: manifest.name,
1113
+ scope: opts.scope,
1114
+ failures: partialDeployFailures,
1115
+ });
1116
+ } catch {
1117
+ // Best-effort: alert path failure must not break install.
1072
1118
  }
1073
1119
  }
1074
1120
 
@@ -23,9 +23,10 @@
23
23
  import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
24
24
  import { homedir } from 'node:os';
25
25
  import { join, dirname } from 'node:path';
26
- import { randomBytes } from 'node:crypto';
27
26
 
28
27
  import { withFsLock } from './fs-lock.js';
28
+ // v1.5.0 audit-LOW-update-#13: shared tmp-suffix helper.
29
+ import { tmpSuffix } from './lib/tmp-suffix.js';
29
30
 
30
31
  const STATE_REL = ['.ijfw', 'state', 'extension-quotas.json'];
31
32
 
@@ -105,7 +106,8 @@ export async function writeQuotaState(home, state) {
105
106
  const h = home || homeFromOpts({});
106
107
  const path = statePath(h);
107
108
  await mkdir(dirname(path), { recursive: true });
108
- const tmp = `${path}.tmp.${randomBytes(4).toString('hex')}`;
109
+ // v1.5.0 audit-LOW-update-#13: tmpSuffix() replaces inline randomBytes call.
110
+ const tmp = `${path}.tmp.${tmpSuffix({ bytes: 4, includePid: false })}`;
109
111
  await writeFile(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8');
110
112
  await rename(tmp, path);
111
113
  }