@ijfw/memory-server 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +1 -1
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +550 -14
- package/src/cross-orchestrator.js +1171 -10
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-planning.html +273 -0
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +17 -2
- package/src/dashboard-server.js +152 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +27 -1
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +323 -0
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +554 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +152 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/obsidian-parser.js +91 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +10 -0
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +249 -0
- package/src/orchestrator/review.js +136 -0
- package/src/orchestrator/runtime-loop.js +430 -0
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1764 -0
- package/src/orchestrator/status-protocol.js +235 -0
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +281 -0
- package/src/orchestrator/wave-state.js +564 -0
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +656 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -0
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +603 -59
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +113 -12
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +41 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +41 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +278 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* merge-block-aware.js — v1.5.0 T8: in-process port of the block-aware
|
|
3
|
+
* AGENTS.md merger (formerly `claude/skills/ijfw-agents-md/scripts/
|
|
4
|
+
* merge-block-aware.sh`).
|
|
5
|
+
*
|
|
6
|
+
* RATIONALE — why this lives in mcp-server/src/orchestrator and not in the
|
|
7
|
+
* skill scripts dir:
|
|
8
|
+
* The shell script was invoked under `withFsLock(.AGENTS.md.lock)` via
|
|
9
|
+
* `execFile('bash', …)`. Holding an `fs-lock` across a subprocess spawn is a
|
|
10
|
+
* STATE-SDK-CONTRACT §3 violation:
|
|
11
|
+
* "No lock is held across a subprocess spawn. `merge-block-aware.sh` is
|
|
12
|
+
* ported to in-process JS (T8) …"
|
|
13
|
+
* This module is that port. The shell script remains on disk for parity
|
|
14
|
+
* testing in this commit; T14 (SDK grep-gate sweep) decides its long-term
|
|
15
|
+
* fate.
|
|
16
|
+
*
|
|
17
|
+
* SCOPE
|
|
18
|
+
* - Pure-JS replacement for the byte-stable marker-block replacement that
|
|
19
|
+
* the shell script did with a here-doc `node -e '…'` payload. Same
|
|
20
|
+
* marker pattern (`<!-- IJFW-<BLOCK>-START -->` / `…-END -->`), same
|
|
21
|
+
* append-on-missing-pair fallback, same content-wrapping (`\n…\n`).
|
|
22
|
+
* - Atomic write via `writeAtomic` (lib/atomic-io.js) — tmp + rename — so
|
|
23
|
+
* `mergeFile()` can be called inside a `withFsLock` critical section.
|
|
24
|
+
* - Template seeding (copies AGENTS.md.tmpl when the target is absent).
|
|
25
|
+
* - Backup retention (last 3 per project-hash under
|
|
26
|
+
* `~/.ijfw/state/agents-md/backups/<hash>/`), matching shell parity.
|
|
27
|
+
*
|
|
28
|
+
* NON-GOALS
|
|
29
|
+
* - Frontmatter handling — the shell script delegated to
|
|
30
|
+
* `hoist-frontmatter.sh`. Marker-block merge ONLY (parity).
|
|
31
|
+
* - YAML schema validation — owned by the ijfw-agents-md skill's ajv path.
|
|
32
|
+
* - Multi-tier lock acquisition — callers acquire the §3 #8 AGENTS.md lock
|
|
33
|
+
* before calling `mergeFile`. This module never holds a lock itself.
|
|
34
|
+
*
|
|
35
|
+
* RESERVED BLOCK NAMES
|
|
36
|
+
* Matches the shell script verbatim:
|
|
37
|
+
* MEMORY | ROUTING | AGENTS | BLACKBOARD | FRONTMATTER
|
|
38
|
+
*
|
|
39
|
+
* ESM, Node ≥18, zero new prod deps.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
readFileSync, existsSync, copyFileSync, mkdirSync, statSync, readdirSync,
|
|
44
|
+
unlinkSync,
|
|
45
|
+
} from 'node:fs';
|
|
46
|
+
import { join, dirname, resolve as pathResolve } from 'node:path';
|
|
47
|
+
import { homedir } from 'node:os';
|
|
48
|
+
import { createHash } from 'node:crypto';
|
|
49
|
+
import { fileURLToPath } from 'node:url';
|
|
50
|
+
|
|
51
|
+
import { writeAtomic } from '../lib/atomic-io.js';
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Constants
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The frozen set of block names the merger accepts. Matches the shell
|
|
59
|
+
* script's `is_pair_form` + arg-validation case statements. An unknown block
|
|
60
|
+
* name yields `ERR_BAD_BLOCK` (the same exit-2 the shell script returns).
|
|
61
|
+
*/
|
|
62
|
+
export const RESERVED_BLOCKS = Object.freeze([
|
|
63
|
+
'MEMORY', 'ROUTING', 'AGENTS', 'BLACKBOARD', 'FRONTMATTER',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const RESERVED_BLOCK_SET = new Set(RESERVED_BLOCKS);
|
|
67
|
+
|
|
68
|
+
/** Retention cap on backup files per project-hash (shell parity: keep 3). */
|
|
69
|
+
const BACKUP_RETAIN = 3;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Default template path (relative to this module). Used when `mergeFile` is
|
|
73
|
+
* called without `templatePath` and the target file does not exist.
|
|
74
|
+
*/
|
|
75
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
76
|
+
const __dirname = dirname(__filename);
|
|
77
|
+
const DEFAULT_TEMPLATE = pathResolve(
|
|
78
|
+
__dirname,
|
|
79
|
+
'..', '..', '..', 'claude', 'skills', 'ijfw-agents-md', 'templates',
|
|
80
|
+
'AGENTS.md.tmpl',
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Error class
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Typed error so callers can distinguish bad-input refusal from an IO error.
|
|
89
|
+
* `code` mirrors the shell script's exit-code semantics:
|
|
90
|
+
* - 'ERR_BAD_BLOCK' — block name not in RESERVED_BLOCKS (sh: exit 2)
|
|
91
|
+
* - 'ERR_TEMPLATE_MISSING' — target absent + no template available (sh: 3)
|
|
92
|
+
* - 'ERR_BAD_PAYLOAD' — pairs argument malformed (sh: 2)
|
|
93
|
+
*/
|
|
94
|
+
export class MergeBlockAwareError extends Error {
|
|
95
|
+
constructor(code, message) {
|
|
96
|
+
super(message);
|
|
97
|
+
this.name = 'MergeBlockAwareError';
|
|
98
|
+
this.code = code;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Core merge — pure string → string. The hot path that the shell script's
|
|
104
|
+
// here-doc `node -e '…'` payload performed verbatim. Held under no lock.
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Apply each `{block, content}` pair to `src`, producing the new file body.
|
|
109
|
+
*
|
|
110
|
+
* Semantics (verbatim port of merge-block-aware.sh's node here-doc):
|
|
111
|
+
* - If `<!-- IJFW-<block>-START -->` and `…-END -->` both exist AND the
|
|
112
|
+
* end-marker appears after the start-marker, replace the bytes BETWEEN
|
|
113
|
+
* them with `\n<content>\n` (or just `\n` when content is empty/null).
|
|
114
|
+
* Marker-positioning rules are identical to the shell port: `indexOf`
|
|
115
|
+
* for both markers, `endIdx > startIdx` to accept.
|
|
116
|
+
* - Otherwise (markers absent or out of order), APPEND a fresh marker pair
|
|
117
|
+
* containing the new content to the end of `src`. A leading newline is
|
|
118
|
+
* inserted if `src` does not already end with one (parity with the
|
|
119
|
+
* `sep = src.endsWith("\n") ? "" : "\n"` ternary).
|
|
120
|
+
* - Pairs are applied in order; the result of pair N becomes input to N+1.
|
|
121
|
+
* (Matches the shell loop `for (let i = 0; i < count; i++) …`.)
|
|
122
|
+
*
|
|
123
|
+
* Pure function: no I/O, no spawn, no locks. Idempotent on the same input.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} src the current file contents (utf-8)
|
|
126
|
+
* @param {Array<{block: string, content: string}>} pairs
|
|
127
|
+
* @returns {string} the new file contents
|
|
128
|
+
* @throws {MergeBlockAwareError} on an unknown block name
|
|
129
|
+
*/
|
|
130
|
+
export function mergeBlocks(src, pairs) {
|
|
131
|
+
if (typeof src !== 'string') {
|
|
132
|
+
throw new MergeBlockAwareError(
|
|
133
|
+
'ERR_BAD_PAYLOAD', `mergeBlocks: src must be a string (got ${typeof src})`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (!Array.isArray(pairs)) {
|
|
137
|
+
throw new MergeBlockAwareError(
|
|
138
|
+
'ERR_BAD_PAYLOAD', 'mergeBlocks: pairs must be an array',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
let out = src;
|
|
142
|
+
for (const pair of pairs) {
|
|
143
|
+
if (!pair || typeof pair !== 'object') {
|
|
144
|
+
throw new MergeBlockAwareError(
|
|
145
|
+
'ERR_BAD_PAYLOAD', 'mergeBlocks: each pair must be an object',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const block = pair.block;
|
|
149
|
+
if (typeof block !== 'string' || !RESERVED_BLOCK_SET.has(block)) {
|
|
150
|
+
throw new MergeBlockAwareError(
|
|
151
|
+
'ERR_BAD_BLOCK',
|
|
152
|
+
`mergeBlocks: block name reserved set: ${RESERVED_BLOCKS.join(' ')} (got ${JSON.stringify(block)})`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const content = (pair.content === undefined || pair.content === null)
|
|
156
|
+
? ''
|
|
157
|
+
: String(pair.content);
|
|
158
|
+
|
|
159
|
+
const startM = `<!-- IJFW-${block}-START -->`;
|
|
160
|
+
const endM = `<!-- IJFW-${block}-END -->`;
|
|
161
|
+
const startIdx = out.indexOf(startM);
|
|
162
|
+
const endIdx = out.indexOf(endM);
|
|
163
|
+
|
|
164
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
165
|
+
const before = out.slice(0, startIdx + startM.length);
|
|
166
|
+
const after = out.slice(endIdx);
|
|
167
|
+
const inner = content && content.length ? `\n${content}\n` : '\n';
|
|
168
|
+
out = before + inner + after;
|
|
169
|
+
} else {
|
|
170
|
+
const sep = out.endsWith('\n') ? '' : '\n';
|
|
171
|
+
const inner = content && content.length ? `\n${content}\n` : '\n';
|
|
172
|
+
out = `${out}${sep}\n${startM}${inner}${endM}\n`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Backup retention — parity with the shell `node -e '…'` block that the script
|
|
180
|
+
// invokes alongside `cp`. Pure, deterministic, swallows io errors (best-effort
|
|
181
|
+
// in parity with the shell `2>/dev/null || true`).
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/** sha256(targetDir).slice(0, 12) — matches the shell script's project-hash. */
|
|
185
|
+
export function projectHash(targetDir) {
|
|
186
|
+
return createHash('sha256').update(String(targetDir)).digest('hex').slice(0, 12);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Compute the canonical backup directory for `targetAbsPath`.
|
|
191
|
+
*
|
|
192
|
+
* Shell parity: `~/.ijfw/state/agents-md/backups/<projectHash(targetDir)>/`
|
|
193
|
+
* where `<targetDir>` is the realpath of `dirname(targetAbsPath)` and
|
|
194
|
+
* `<hash>` is sha256 hex (12 chars).
|
|
195
|
+
*/
|
|
196
|
+
export function backupDirFor(targetAbsPath, opts = {}) {
|
|
197
|
+
const targetDir = dirname(pathResolve(targetAbsPath));
|
|
198
|
+
const home = opts.homeDir || homedir();
|
|
199
|
+
return join(home, '.ijfw', 'state', 'agents-md', 'backups', projectHash(targetDir));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Take a millisecond-timestamped backup of `targetAbsPath` into the canonical
|
|
204
|
+
* backup directory, then prune older entries past `retain` (newest-first).
|
|
205
|
+
*
|
|
206
|
+
* Mirrors the shell script's `cp` + `node -e '…sort by mtime…'` block. Best-
|
|
207
|
+
* effort: any IO failure is swallowed so a backup hiccup never blocks the
|
|
208
|
+
* merge (the shell script does `2>/dev/null || true`).
|
|
209
|
+
*
|
|
210
|
+
* @param {string} targetAbsPath absolute path of the target being merged
|
|
211
|
+
* @param {{homeDir?: string, retain?: number, now?: () => number}} [opts]
|
|
212
|
+
* @returns {{taken: boolean, path?: string, pruned: number}}
|
|
213
|
+
*/
|
|
214
|
+
export function rotateBackups(targetAbsPath, opts = {}) {
|
|
215
|
+
const retain = typeof opts.retain === 'number' ? opts.retain : BACKUP_RETAIN;
|
|
216
|
+
const now = typeof opts.now === 'function' ? opts.now : Date.now;
|
|
217
|
+
try {
|
|
218
|
+
if (!existsSync(targetAbsPath)) return { taken: false, pruned: 0 };
|
|
219
|
+
const st = statSync(targetAbsPath);
|
|
220
|
+
if (!st || !st.size) return { taken: false, pruned: 0 };
|
|
221
|
+
const dir = backupDirFor(targetAbsPath, opts);
|
|
222
|
+
try { mkdirSync(dir, { recursive: true }); } catch { /* best-effort */ }
|
|
223
|
+
const backupName = `AGENTS.md.bak.${String(now())}`;
|
|
224
|
+
const backupPath = join(dir, backupName);
|
|
225
|
+
try { copyFileSync(targetAbsPath, backupPath); } catch { return { taken: false, pruned: 0 }; }
|
|
226
|
+
|
|
227
|
+
// Retention — keep newest `retain`, unlink the rest. mtime-sorted to
|
|
228
|
+
// match the shell node payload's `sort((a,b) => b.mt - a.mt)`.
|
|
229
|
+
let pruned = 0;
|
|
230
|
+
try {
|
|
231
|
+
const entries = readdirSync(dir)
|
|
232
|
+
.filter((n) => n.startsWith('AGENTS.md.bak.'))
|
|
233
|
+
.map((n) => {
|
|
234
|
+
try { return { n, mt: statSync(join(dir, n)).mtimeMs }; }
|
|
235
|
+
catch { return null; }
|
|
236
|
+
})
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.sort((a, b) => b.mt - a.mt);
|
|
239
|
+
for (const e of entries.slice(retain)) {
|
|
240
|
+
try { unlinkSync(join(dir, e.n)); pruned += 1; } catch { /* best-effort */ }
|
|
241
|
+
}
|
|
242
|
+
} catch { /* best-effort */ }
|
|
243
|
+
|
|
244
|
+
return { taken: true, path: backupPath, pruned };
|
|
245
|
+
} catch {
|
|
246
|
+
return { taken: false, pruned: 0 };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// mergeFile — the high-level call. Reads, merges, writes-atomically.
|
|
252
|
+
// Held under no lock itself; the caller is expected to wrap it in
|
|
253
|
+
// `withFsLock` if concurrent writers are possible. (`agents-md-blackboard.js`
|
|
254
|
+
// does exactly that, under the §3 #8 AGENTS.md lock.)
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Apply `pairs` to the file at `targetAbsPath` and rewrite atomically.
|
|
259
|
+
*
|
|
260
|
+
* Semantics (verbatim parity with merge-block-aware.sh):
|
|
261
|
+
* - If the target is absent, seed from `opts.templatePath` (default:
|
|
262
|
+
* `claude/skills/ijfw-agents-md/templates/AGENTS.md.tmpl` resolved
|
|
263
|
+
* relative to this module). A missing template throws
|
|
264
|
+
* ERR_TEMPLATE_MISSING.
|
|
265
|
+
* - Take a backup + prune older entries (best-effort).
|
|
266
|
+
* - Apply `mergeBlocks` to the current contents.
|
|
267
|
+
* - Write atomically via `writeAtomic` (tmp + rename).
|
|
268
|
+
*
|
|
269
|
+
* No subprocess at any point — the shell script's `cp` + `node -e '…'`
|
|
270
|
+
* pipeline is fully expressed in JS here.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} targetAbsPath absolute path to the merge target
|
|
273
|
+
* @param {Array<{block: string, content: string}>} pairs
|
|
274
|
+
* @param {{
|
|
275
|
+
* templatePath?: string,
|
|
276
|
+
* homeDir?: string,
|
|
277
|
+
* retain?: number,
|
|
278
|
+
* now?: () => number,
|
|
279
|
+
* backups?: boolean,
|
|
280
|
+
* }} [opts]
|
|
281
|
+
* @returns {{ ok: true, path: string, bytes: number, backup?: string, seeded: boolean }}
|
|
282
|
+
*/
|
|
283
|
+
export function mergeFile(targetAbsPath, pairs, opts = {}) {
|
|
284
|
+
if (typeof targetAbsPath !== 'string' || !targetAbsPath) {
|
|
285
|
+
throw new MergeBlockAwareError(
|
|
286
|
+
'ERR_BAD_PAYLOAD', 'mergeFile: targetAbsPath must be a non-empty string',
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (!Array.isArray(pairs) || pairs.length === 0) {
|
|
290
|
+
throw new MergeBlockAwareError(
|
|
291
|
+
'ERR_BAD_PAYLOAD', 'mergeFile: pairs must be a non-empty array',
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const abs = pathResolve(targetAbsPath);
|
|
295
|
+
|
|
296
|
+
// Seed from template if absent. Shell script behaviour: `cp <template> <target>`
|
|
297
|
+
// on missing target.
|
|
298
|
+
let seeded = false;
|
|
299
|
+
if (!existsSync(abs)) {
|
|
300
|
+
const templatePath = opts.templatePath || DEFAULT_TEMPLATE;
|
|
301
|
+
if (!existsSync(templatePath)) {
|
|
302
|
+
throw new MergeBlockAwareError(
|
|
303
|
+
'ERR_TEMPLATE_MISSING',
|
|
304
|
+
`mergeFile: template missing at ${templatePath}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const dir = dirname(abs);
|
|
308
|
+
try { mkdirSync(dir, { recursive: true }); } catch { /* best-effort */ }
|
|
309
|
+
copyFileSync(templatePath, abs);
|
|
310
|
+
seeded = true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Backup + retention (best-effort, defaults on). `opts.backups === false`
|
|
314
|
+
// suppresses (used by some tests to keep tmp clean).
|
|
315
|
+
let backup;
|
|
316
|
+
if (opts.backups !== false) {
|
|
317
|
+
const rot = rotateBackups(abs, opts);
|
|
318
|
+
if (rot.taken && rot.path) backup = rot.path;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const src = readFileSync(abs, 'utf8');
|
|
322
|
+
const next = mergeBlocks(src, pairs);
|
|
323
|
+
const res = writeAtomic(abs, next, { mode: 0o644, ensureDir: true });
|
|
324
|
+
return {
|
|
325
|
+
ok: true, path: res.path, bytes: res.bytes, backup, seeded,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Convenience: extract the inner content of a marker block from a string.
|
|
331
|
+
// Used by tests + parity checks; not part of the write path.
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Extract the inner content of `block` from `src`, exclusive of marker bytes.
|
|
336
|
+
* Returns null when the markers are absent or in the wrong order.
|
|
337
|
+
*
|
|
338
|
+
* @param {string} src
|
|
339
|
+
* @param {string} block one of RESERVED_BLOCKS
|
|
340
|
+
* @returns {string | null}
|
|
341
|
+
*/
|
|
342
|
+
export function readBlock(src, block) {
|
|
343
|
+
if (!RESERVED_BLOCK_SET.has(block)) return null;
|
|
344
|
+
const startM = `<!-- IJFW-${block}-START -->`;
|
|
345
|
+
const endM = `<!-- IJFW-${block}-END -->`;
|
|
346
|
+
const s = src.indexOf(startM);
|
|
347
|
+
const e = src.indexOf(endM);
|
|
348
|
+
if (s === -1 || e === -1 || e <= s) return null;
|
|
349
|
+
return src.slice(s + startM.length, e);
|
|
350
|
+
}
|