@ijfw/memory-server 1.4.4 → 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 +1016 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -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 +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- 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 +38 -3
- 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 +84 -17
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- 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 +94 -17
- 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
|
@@ -1,50 +1,236 @@
|
|
|
1
1
|
// --- cross-project-search: BM25 search across every registered IJFW project ---
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// server.js
|
|
3
|
+
// Canonical cross-project search surface. The legacy naive-keyword-count
|
|
4
|
+
// `searchAcrossProjects` in server.js was removed in v1.5.1 H1.5 (audit
|
|
5
|
+
// finding memory-engine.md F-FUN-4) — `scope:'all'` on the MCP search tool
|
|
6
|
+
// now routes here. Builds a corpus of (project, source, line) docs from each
|
|
5
7
|
// registered project's memory files, hands it to the BM25 ranker in
|
|
6
8
|
// search-bm25.js, and returns hits tagged with [project:<basename>].
|
|
7
9
|
//
|
|
8
10
|
// Pure + injectable. The caller supplies the registry reader and the
|
|
9
11
|
// per-project memory reader so this module can be unit-tested without
|
|
10
12
|
// touching the home directory.
|
|
13
|
+
//
|
|
14
|
+
// v1.5.0 audit-H3.4 (memory-engine.md F-SEC-1): registry entries are
|
|
15
|
+
// treated as UNTRUSTED. Each entry.path is:
|
|
16
|
+
// 1. realpath-resolved (symlinks resolved against the filesystem)
|
|
17
|
+
// 2. containment-checked against `allowedRoots` (default: $HOME)
|
|
18
|
+
// 3. silently skipped if it escapes the allowed roots OR fails realpath
|
|
19
|
+
// 4. logged to stderr ONCE per unique offending raw-path (Set dedup)
|
|
20
|
+
// Threat model: a compromised dep / malicious project that writes a
|
|
21
|
+
// registry entry pointing at /etc/passwd via a symlink in the user's
|
|
22
|
+
// home directory will be caught by step 1+2. Plain `isAbsolute()` was
|
|
23
|
+
// the only prior validation and was insufficient.
|
|
11
24
|
|
|
12
|
-
import { basename } from 'node:path';
|
|
25
|
+
import { basename, resolve, join } from 'node:path';
|
|
26
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
13
28
|
import { searchCorpus } from './search-bm25.js';
|
|
14
29
|
|
|
30
|
+
// v1.5.0 audit MED #10 (memory-engine.md F-SPD-2): per-project corpus
|
|
31
|
+
// cache keyed on (canonicalProjectPath, signature) where `signature` is
|
|
32
|
+
// the joined mtimes of the project's memory files. As long as nothing
|
|
33
|
+
// has changed under .ijfw/memory the cache hits and we skip the
|
|
34
|
+
// readProjectMemory() call entirely. Cache is module-local; tests can
|
|
35
|
+
// reset via _resetCorpusCache(). Cap at 64 entries (LRU-ish via Map
|
|
36
|
+
// insertion order) so a long-lived dashboard process can't OOM the cache.
|
|
37
|
+
const CORPUS_CACHE = new Map();
|
|
38
|
+
const CORPUS_CACHE_CAP = 64;
|
|
39
|
+
|
|
40
|
+
/** Reset the corpus mtime cache. Test-only. */
|
|
41
|
+
export function _resetCorpusCache() {
|
|
42
|
+
CORPUS_CACHE.clear();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Files that participate in the cache signature. Match the readers used
|
|
46
|
+
// by readProjectMemory in server.js so an edit to any of them busts the
|
|
47
|
+
// cache. Missing files contribute "0" -- creation of a previously-missing
|
|
48
|
+
// file is a cache-bust on its own.
|
|
49
|
+
const CACHE_SIGNATURE_FILES = [
|
|
50
|
+
['.ijfw', 'memory', 'knowledge.md'],
|
|
51
|
+
['.ijfw', 'memory', 'project-journal.md'],
|
|
52
|
+
['.ijfw', 'memory', 'handoff.md'],
|
|
53
|
+
['.ijfw', 'memory', 'team-knowledge.md'],
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function computeProjectSignature(canonicalProjectPath) {
|
|
57
|
+
let sig = '';
|
|
58
|
+
for (const parts of CACHE_SIGNATURE_FILES) {
|
|
59
|
+
const p = join(canonicalProjectPath, ...parts);
|
|
60
|
+
try {
|
|
61
|
+
const s = statSync(p);
|
|
62
|
+
sig += `${s.mtimeMs.toFixed(3)}:${s.size}|`;
|
|
63
|
+
} catch {
|
|
64
|
+
sig += '0|';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return sig;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getCachedDocs(canonicalProjectPath) {
|
|
71
|
+
const sig = computeProjectSignature(canonicalProjectPath);
|
|
72
|
+
const cached = CORPUS_CACHE.get(canonicalProjectPath);
|
|
73
|
+
if (cached && cached.sig === sig) {
|
|
74
|
+
// LRU touch: re-insert at the end of the Map iteration order.
|
|
75
|
+
CORPUS_CACHE.delete(canonicalProjectPath);
|
|
76
|
+
CORPUS_CACHE.set(canonicalProjectPath, cached);
|
|
77
|
+
return cached.docs;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setCachedDocs(canonicalProjectPath, docs) {
|
|
83
|
+
const sig = computeProjectSignature(canonicalProjectPath);
|
|
84
|
+
CORPUS_CACHE.set(canonicalProjectPath, { sig, docs });
|
|
85
|
+
// Trim oldest entry if we're over the cap. Map iteration is insertion
|
|
86
|
+
// order so the first key is the oldest.
|
|
87
|
+
if (CORPUS_CACHE.size > CORPUS_CACHE_CAP) {
|
|
88
|
+
const oldestKey = CORPUS_CACHE.keys().next().value;
|
|
89
|
+
if (oldestKey !== undefined) CORPUS_CACHE.delete(oldestKey);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Module-level dedup set for "skipped offender" stderr noise control.
|
|
94
|
+
// Keyed by the raw entry.path (the attacker-controlled string) so the same
|
|
95
|
+
// malicious entry is reported only once per process lifetime. Exported for
|
|
96
|
+
// test reset via `_resetSkipLog()`.
|
|
97
|
+
const SKIP_LOG = new Set();
|
|
98
|
+
|
|
99
|
+
/** Reset the skip-log dedup set. Test-only. */
|
|
100
|
+
export function _resetSkipLog() {
|
|
101
|
+
SKIP_LOG.clear();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Default allowed roots: just the user's home directory.
|
|
105
|
+
* Tests can pass `opts.allowedRoots` to constrain or widen.
|
|
106
|
+
* Returned as resolved (realpath'd) absolute paths so containment
|
|
107
|
+
* comparison is apples-to-apples with the realpath'd entry. */
|
|
108
|
+
function defaultAllowedRoots() {
|
|
109
|
+
const roots = [];
|
|
110
|
+
try {
|
|
111
|
+
const home = homedir();
|
|
112
|
+
if (home && typeof home === 'string') {
|
|
113
|
+
// realpath the root itself so /var/root on macOS resolves to
|
|
114
|
+
// /private/var/root (same canonicalization as the entry side).
|
|
115
|
+
try { roots.push(realpathSync(home)); } catch { roots.push(resolve(home)); }
|
|
116
|
+
}
|
|
117
|
+
} catch { /* ignore */ }
|
|
118
|
+
return roots;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** True iff `child` is `root` or a path nested under `root`.
|
|
122
|
+
* Both inputs are expected to be already-canonical absolute paths.
|
|
123
|
+
* We compare with a trailing-separator guard so `/home/alice-evil`
|
|
124
|
+
* is NOT considered inside `/home/alice`. */
|
|
125
|
+
function isUnder(child, root) {
|
|
126
|
+
if (!child || !root) return false;
|
|
127
|
+
if (child === root) return true;
|
|
128
|
+
// Use both POSIX and Windows separators defensively. Path normalization
|
|
129
|
+
// has already happened via realpathSync; we just need the boundary check.
|
|
130
|
+
const withPosixSep = root.endsWith('/') ? root : root + '/';
|
|
131
|
+
const withWindowsSep = root.endsWith('\\') ? root : root + '\\';
|
|
132
|
+
return child.startsWith(withPosixSep) || child.startsWith(withWindowsSep);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Resolve + containment check.
|
|
136
|
+
* Returns the canonical path on success, or null if the entry must be
|
|
137
|
+
* skipped. On skip, logs to stderr once per unique raw path. */
|
|
138
|
+
function safeResolveProjectPath(rawPath, allowedRoots) {
|
|
139
|
+
if (!rawPath || typeof rawPath !== 'string') {
|
|
140
|
+
return _skip(rawPath, 'not-a-string');
|
|
141
|
+
}
|
|
142
|
+
let canonical;
|
|
143
|
+
try {
|
|
144
|
+
// realpathSync resolves symlinks. If the entry points to a symlink
|
|
145
|
+
// chain whose target escapes allowedRoots, this is where we catch it.
|
|
146
|
+
canonical = realpathSync(rawPath);
|
|
147
|
+
} catch {
|
|
148
|
+
// ENOENT / EACCES / ELOOP — entry path doesn't exist or is unreadable.
|
|
149
|
+
// Graceful skip; don't throw (a stale registry shouldn't crash search).
|
|
150
|
+
return _skip(rawPath, 'realpath-failed');
|
|
151
|
+
}
|
|
152
|
+
if (!allowedRoots.some(root => isUnder(canonical, root))) {
|
|
153
|
+
return _skip(rawPath, `escapes-allowed-roots (realpath=${canonical})`);
|
|
154
|
+
}
|
|
155
|
+
return canonical;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function _skip(rawPath, reason) {
|
|
159
|
+
const key = String(rawPath || '<null>');
|
|
160
|
+
if (!SKIP_LOG.has(key)) {
|
|
161
|
+
SKIP_LOG.add(key);
|
|
162
|
+
try {
|
|
163
|
+
// Single-line, stderr-only — same posture as the rest of the engine's
|
|
164
|
+
// advisory warnings. Production callers (Claude Code) capture stderr
|
|
165
|
+
// and surface in the dashboard's diagnostics tab.
|
|
166
|
+
process.stderr.write(
|
|
167
|
+
`[ijfw cross-project-search] skipped registry entry: ${reason}: ${key}\n`
|
|
168
|
+
);
|
|
169
|
+
} catch { /* never let a logging failure break search */ }
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
15
174
|
// Build a corpus of line-level docs from the provided projects.
|
|
16
175
|
// projects: [{ path, hash?, iso? }]
|
|
17
176
|
// readProjectMemory(path) -> { knowledge, journal, handoff } (strings)
|
|
177
|
+
// opts.allowedRoots: optional array of canonical absolute roots; entries
|
|
178
|
+
// whose realpath escapes ALL of them are skipped.
|
|
179
|
+
// Default: [realpath(homedir())].
|
|
18
180
|
// Returns [{ id, text, meta }] where meta carries project + source + lineNo.
|
|
19
|
-
export function buildCorpus(projects, readProjectMemory) {
|
|
181
|
+
export function buildCorpus(projects, readProjectMemory, opts = {}) {
|
|
182
|
+
const allowedRoots = Array.isArray(opts.allowedRoots) && opts.allowedRoots.length
|
|
183
|
+
? opts.allowedRoots
|
|
184
|
+
: defaultAllowedRoots();
|
|
20
185
|
const docs = [];
|
|
21
186
|
for (const entry of projects) {
|
|
22
|
-
|
|
23
|
-
const
|
|
187
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
188
|
+
const canonical = safeResolveProjectPath(entry.path, allowedRoots);
|
|
189
|
+
if (canonical === null) continue; // skipped (symlink-escape or missing)
|
|
190
|
+
|
|
191
|
+
// v1.5.0 audit MED #10: try the mtime cache before re-reading.
|
|
192
|
+
// The cache is opt-out via opts.useCache=false (tests / consistency runs).
|
|
193
|
+
const useCache = opts.useCache !== false;
|
|
194
|
+
let projectDocs = useCache ? getCachedDocs(canonical) : null;
|
|
195
|
+
if (projectDocs) {
|
|
196
|
+
for (const d of projectDocs) docs.push(d);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const tag = basename(canonical);
|
|
201
|
+
// Pass the CANONICAL path to the reader, not the raw one — so a reader
|
|
202
|
+
// that joins with `.ijfw/memory/...` walks the canonical tree, not a
|
|
203
|
+
// possibly-spoofed symlink chain.
|
|
204
|
+
const mem = readProjectMemory(canonical) || {};
|
|
205
|
+
projectDocs = [];
|
|
24
206
|
for (const [source, content] of Object.entries(mem)) {
|
|
25
207
|
if (typeof content !== 'string' || content.length === 0) continue;
|
|
26
208
|
const lines = content.split('\n');
|
|
27
209
|
for (let i = 0; i < lines.length; i++) {
|
|
28
210
|
const line = lines[i];
|
|
29
211
|
if (line.trim().length === 0) continue;
|
|
30
|
-
|
|
212
|
+
projectDocs.push({
|
|
31
213
|
id: `${tag}:${source}:${i + 1}`,
|
|
32
214
|
text: line,
|
|
33
|
-
meta: { project: tag, projectPath:
|
|
215
|
+
meta: { project: tag, projectPath: canonical, source, lineNo: i + 1 },
|
|
34
216
|
});
|
|
35
217
|
}
|
|
36
218
|
}
|
|
219
|
+
if (useCache) setCachedDocs(canonical, projectDocs);
|
|
220
|
+
for (const d of projectDocs) docs.push(d);
|
|
37
221
|
}
|
|
38
222
|
return docs;
|
|
39
223
|
}
|
|
40
224
|
|
|
41
225
|
// Run a BM25-ranked search across the corpus produced from `projects`.
|
|
42
226
|
// Returns [{ content, source, line, project, score, snippet }], capped at limit.
|
|
227
|
+
// opts.allowedRoots: forwarded to buildCorpus (see above).
|
|
228
|
+
// opts.limit: 1..50, default 10.
|
|
43
229
|
export function crossProjectSearch(query, projects, readProjectMemory, opts = {}) {
|
|
44
230
|
const limit = clamp(opts.limit, 1, 50, 10);
|
|
45
231
|
if (!query || typeof query !== 'string') return [];
|
|
46
232
|
|
|
47
|
-
const docs = buildCorpus(projects, readProjectMemory);
|
|
233
|
+
const docs = buildCorpus(projects, readProjectMemory, opts);
|
|
48
234
|
if (docs.length === 0) return [];
|
|
49
235
|
|
|
50
236
|
const hits = searchCorpus(query, docs, { limit });
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>IJFW · Waves</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: light dark; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body { margin: 0; font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #fafafa; color: #222; }
|
|
11
|
+
header { padding: 12px 20px; border-bottom: 1px solid #ddd; background: #fff; }
|
|
12
|
+
header h1 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
13
|
+
header .sub { color: #777; font-size: 12px; margin-top: 4px; }
|
|
14
|
+
main { padding: 20px; max-width: 1200px; margin: 0 auto; }
|
|
15
|
+
.layout { display: grid; grid-template-columns: 320px 1fr; gap: 20px; min-height: 500px; }
|
|
16
|
+
.sidebar { background: #fff; border: 1px solid #ddd; border-radius: 6px; overflow: hidden; }
|
|
17
|
+
.sidebar-header { padding: 10px 14px; font-weight: 600; border-bottom: 1px solid #eee; font-size: 13px; color: #555; }
|
|
18
|
+
.wave-list { max-height: 70vh; overflow-y: auto; }
|
|
19
|
+
.wave-row { padding: 10px 14px; border-bottom: 1px solid #f0f0f0; cursor: pointer; display: grid; grid-template-columns: 1fr auto; gap: 4px; }
|
|
20
|
+
.wave-row:hover { background: #f4f8ff; }
|
|
21
|
+
.wave-row.selected { background: #eaf3ff; }
|
|
22
|
+
.wave-row .id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; font-weight: 600; }
|
|
23
|
+
.wave-row .status { font-size: 11px; padding: 1px 6px; border-radius: 3px; background: #eee; color: #555; text-transform: uppercase; align-self: center; }
|
|
24
|
+
.wave-row.status-done .status { background: #d6f5d6; color: #1a6b1a; }
|
|
25
|
+
.wave-row.status-in_progress .status { background: #fff4cc; color: #7a5a00; }
|
|
26
|
+
.wave-row.status-blocked .status { background: #fbd5d5; color: #8a1a1a; }
|
|
27
|
+
.wave-row .ts { grid-column: 1 / -1; font-size: 11px; color: #888; }
|
|
28
|
+
.viewer { background: #fff; padding: 24px 28px; border: 1px solid #ddd; border-radius: 6px; min-height: 500px; }
|
|
29
|
+
.meta { font-size: 12px; color: #666; margin-bottom: 16px; padding-bottom: 10px; border-bottom: 1px solid #eee; }
|
|
30
|
+
.meta .meta-row { display: inline-block; margin-right: 16px; }
|
|
31
|
+
.meta strong { color: #333; }
|
|
32
|
+
.doc h1 { font-size: 22px; margin: 0 0 12px; }
|
|
33
|
+
.doc h2 { font-size: 18px; margin: 24px 0 10px; padding-bottom: 4px; border-bottom: 1px solid #eee; }
|
|
34
|
+
.doc h3 { font-size: 15px; margin: 20px 0 8px; }
|
|
35
|
+
.doc pre { background: #f4f4f4; padding: 12px; border-radius: 4px; overflow-x: auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
36
|
+
.doc code { background: #f4f4f4; padding: 1px 4px; border-radius: 3px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
37
|
+
.doc pre code { background: transparent; padding: 0; }
|
|
38
|
+
.doc table { border-collapse: collapse; margin: 12px 0; }
|
|
39
|
+
.doc th, .doc td { border: 1px solid #ddd; padding: 6px 10px; text-align: left; font-size: 13px; }
|
|
40
|
+
.doc th { background: #f4f4f4; }
|
|
41
|
+
.doc blockquote { border-left: 3px solid #ccc; padding-left: 12px; color: #666; margin: 12px 0; }
|
|
42
|
+
.err { color: #c00; font-style: italic; }
|
|
43
|
+
.hint { color: #777; font-style: italic; }
|
|
44
|
+
@media (prefers-color-scheme: dark) {
|
|
45
|
+
body { background: #1a1a1a; color: #ddd; }
|
|
46
|
+
header, .sidebar, .viewer { background: #222; border-color: #333; }
|
|
47
|
+
.sidebar-header, .meta { border-color: #2e2e2e; color: #aaa; }
|
|
48
|
+
.wave-row { border-color: #2a2a2a; }
|
|
49
|
+
.wave-row:hover { background: #2a2f3a; }
|
|
50
|
+
.wave-row.selected { background: #2c3548; }
|
|
51
|
+
.wave-row .status { background: #333; color: #ccc; }
|
|
52
|
+
.doc pre, .doc code, .doc th { background: #2a2a2a; }
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<header>
|
|
58
|
+
<h1>IJFW · Waves</h1>
|
|
59
|
+
<div class="sub">Live state of .ijfw/wave-*/STATE.md across the current project.</div>
|
|
60
|
+
</header>
|
|
61
|
+
<main>
|
|
62
|
+
<div class="layout">
|
|
63
|
+
<aside class="sidebar">
|
|
64
|
+
<div class="sidebar-header">Waves (newest first)</div>
|
|
65
|
+
<div class="wave-list" id="waveList"></div>
|
|
66
|
+
</aside>
|
|
67
|
+
<section class="viewer">
|
|
68
|
+
<div class="meta" id="waveMeta"></div>
|
|
69
|
+
<div id="doc" class="doc"></div>
|
|
70
|
+
</section>
|
|
71
|
+
</div>
|
|
72
|
+
</main>
|
|
73
|
+
<script>
|
|
74
|
+
// Reused markdown shim from /planning — produces a safe DOMFragment, no innerHTML.
|
|
75
|
+
// Handles headings, paragraphs, code blocks, inline code/bold/italic, links,
|
|
76
|
+
// lists, blockquotes, tables. Not full CommonMark.
|
|
77
|
+
|
|
78
|
+
function setText(el, text) {
|
|
79
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
80
|
+
el.appendChild(document.createTextNode(text));
|
|
81
|
+
}
|
|
82
|
+
function setStatus(el, text, cls) {
|
|
83
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
84
|
+
const div = document.createElement('div');
|
|
85
|
+
div.className = cls;
|
|
86
|
+
div.textContent = text;
|
|
87
|
+
el.appendChild(div);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeNode(tag, text) {
|
|
91
|
+
const el = document.createElement(tag);
|
|
92
|
+
if (text !== undefined) el.appendChild(document.createTextNode(text));
|
|
93
|
+
return el;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderInlineToNodes(s) {
|
|
97
|
+
const nodes = [];
|
|
98
|
+
let i = 0;
|
|
99
|
+
while (i < s.length) {
|
|
100
|
+
if (s[i] === '`') {
|
|
101
|
+
const end = s.indexOf('`', i + 1);
|
|
102
|
+
if (end !== -1) { nodes.push(makeNode('code', s.slice(i + 1, end))); i = end + 1; continue; }
|
|
103
|
+
}
|
|
104
|
+
if (s[i] === '*' && s[i + 1] === '*') {
|
|
105
|
+
const end = s.indexOf('**', i + 2);
|
|
106
|
+
if (end !== -1) { nodes.push(makeNode('strong', s.slice(i + 2, end))); i = end + 2; continue; }
|
|
107
|
+
}
|
|
108
|
+
if (s[i] === '*') {
|
|
109
|
+
const end = s.indexOf('*', i + 1);
|
|
110
|
+
if (end !== -1 && end - i > 1) { nodes.push(makeNode('em', s.slice(i + 1, end))); i = end + 1; continue; }
|
|
111
|
+
}
|
|
112
|
+
if (s[i] === '[') {
|
|
113
|
+
const close = s.indexOf(']', i + 1);
|
|
114
|
+
if (close !== -1 && s[close + 1] === '(') {
|
|
115
|
+
const urlEnd = s.indexOf(')', close + 2);
|
|
116
|
+
if (urlEnd !== -1) {
|
|
117
|
+
const a = document.createElement('a');
|
|
118
|
+
a.textContent = s.slice(i + 1, close);
|
|
119
|
+
// r13-L-01: tightened URL guard. ALLOW http/https + same-origin relative.
|
|
120
|
+
// BLOCK javascript:, data:, mailto:, vbscript:, file:, AND protocol-relative `//`.
|
|
121
|
+
const url = s.slice(close + 2, urlEnd);
|
|
122
|
+
const isAllowed = (
|
|
123
|
+
/^https?:\/\//.test(url) ||
|
|
124
|
+
(!url.startsWith('//') && !/^[^:/?#]+:/.test(url))
|
|
125
|
+
);
|
|
126
|
+
if (isAllowed) { a.href = url; a.target = '_blank'; a.rel = 'noopener'; }
|
|
127
|
+
nodes.push(a);
|
|
128
|
+
i = urlEnd + 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
let j = i;
|
|
134
|
+
while (j < s.length && !'`*['.includes(s[j])) j++;
|
|
135
|
+
if (j === i) j = i + 1;
|
|
136
|
+
nodes.push(document.createTextNode(s.slice(i, j)));
|
|
137
|
+
i = j;
|
|
138
|
+
}
|
|
139
|
+
return nodes;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderMarkdownToFragment(md) {
|
|
143
|
+
const frag = document.createDocumentFragment();
|
|
144
|
+
const lines = md.split('\n');
|
|
145
|
+
let i = 0;
|
|
146
|
+
while (i < lines.length) {
|
|
147
|
+
const line = lines[i];
|
|
148
|
+
if (/^```/.test(line)) {
|
|
149
|
+
const buf = []; i++;
|
|
150
|
+
while (i < lines.length && !/^```/.test(lines[i])) { buf.push(lines[i]); i++; }
|
|
151
|
+
i++;
|
|
152
|
+
const pre = document.createElement('pre');
|
|
153
|
+
const code = document.createElement('code');
|
|
154
|
+
code.textContent = buf.join('\n');
|
|
155
|
+
pre.appendChild(code);
|
|
156
|
+
frag.appendChild(pre);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
160
|
+
if (h) {
|
|
161
|
+
const tag = 'h' + h[1].length;
|
|
162
|
+
const el = document.createElement(tag);
|
|
163
|
+
for (const n of renderInlineToNodes(h[2])) el.appendChild(n);
|
|
164
|
+
frag.appendChild(el); i++; continue;
|
|
165
|
+
}
|
|
166
|
+
if (/^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|?\s*-/.test(lines[i + 1])) {
|
|
167
|
+
const tbl = document.createElement('table');
|
|
168
|
+
const head = line.split('|').slice(1, -1).map((c) => c.trim());
|
|
169
|
+
const tr = document.createElement('tr');
|
|
170
|
+
for (const c of head) {
|
|
171
|
+
const th = document.createElement('th');
|
|
172
|
+
for (const n of renderInlineToNodes(c)) th.appendChild(n);
|
|
173
|
+
tr.appendChild(th);
|
|
174
|
+
}
|
|
175
|
+
tbl.appendChild(tr); i += 2;
|
|
176
|
+
while (i < lines.length && /^\s*\|/.test(lines[i])) {
|
|
177
|
+
const cells = lines[i].split('|').slice(1, -1).map((c) => c.trim());
|
|
178
|
+
const row = document.createElement('tr');
|
|
179
|
+
for (const c of cells) {
|
|
180
|
+
const td = document.createElement('td');
|
|
181
|
+
for (const n of renderInlineToNodes(c)) td.appendChild(n);
|
|
182
|
+
row.appendChild(td);
|
|
183
|
+
}
|
|
184
|
+
tbl.appendChild(row); i++;
|
|
185
|
+
}
|
|
186
|
+
frag.appendChild(tbl); continue;
|
|
187
|
+
}
|
|
188
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
189
|
+
const ul = document.createElement('ul');
|
|
190
|
+
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) {
|
|
191
|
+
const li = document.createElement('li');
|
|
192
|
+
for (const n of renderInlineToNodes(lines[i].replace(/^\s*[-*]\s+/, ''))) li.appendChild(n);
|
|
193
|
+
ul.appendChild(li); i++;
|
|
194
|
+
}
|
|
195
|
+
frag.appendChild(ul); continue;
|
|
196
|
+
}
|
|
197
|
+
if (/^>\s?/.test(line)) {
|
|
198
|
+
const bq = document.createElement('blockquote');
|
|
199
|
+
let first = true;
|
|
200
|
+
while (i < lines.length && /^>\s?/.test(lines[i])) {
|
|
201
|
+
if (!first) bq.appendChild(document.createElement('br'));
|
|
202
|
+
for (const n of renderInlineToNodes(lines[i].replace(/^>\s?/, ''))) bq.appendChild(n);
|
|
203
|
+
first = false; i++;
|
|
204
|
+
}
|
|
205
|
+
frag.appendChild(bq); continue;
|
|
206
|
+
}
|
|
207
|
+
if (line.trim() === '') { i++; continue; }
|
|
208
|
+
const p = document.createElement('p');
|
|
209
|
+
const buf = [line]; i++;
|
|
210
|
+
while (i < lines.length && lines[i].trim() !== '' && !/^(#{1,6}\s|```|>|\s*[-*]\s|\s*\|)/.test(lines[i])) {
|
|
211
|
+
buf.push(lines[i]); i++;
|
|
212
|
+
}
|
|
213
|
+
for (const n of renderInlineToNodes(buf.join(' '))) p.appendChild(n);
|
|
214
|
+
frag.appendChild(p);
|
|
215
|
+
}
|
|
216
|
+
return frag;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------- wave list + viewer ----------
|
|
220
|
+
|
|
221
|
+
let selectedRow = null;
|
|
222
|
+
|
|
223
|
+
function renderMeta(w) {
|
|
224
|
+
const meta = document.getElementById('waveMeta');
|
|
225
|
+
while (meta.firstChild) meta.removeChild(meta.firstChild);
|
|
226
|
+
const fields = [
|
|
227
|
+
['Wave', w.id],
|
|
228
|
+
['Status', w.status],
|
|
229
|
+
['Created', w.created_at ?? '—'],
|
|
230
|
+
['Checkpoint', w.checkpoint_at ?? '—'],
|
|
231
|
+
['Agents', String(w.agents_count ?? 0)],
|
|
232
|
+
['Active claims', String(w.claims_active ?? 0)],
|
|
233
|
+
['Open blockers', String(w.blockers_open ?? 0)],
|
|
234
|
+
];
|
|
235
|
+
for (const [label, val] of fields) {
|
|
236
|
+
const row = document.createElement('span');
|
|
237
|
+
row.className = 'meta-row';
|
|
238
|
+
const lab = document.createElement('strong');
|
|
239
|
+
lab.textContent = label + ': ';
|
|
240
|
+
row.appendChild(lab);
|
|
241
|
+
row.appendChild(document.createTextNode(val));
|
|
242
|
+
meta.appendChild(row);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function loadWaveState(w, row) {
|
|
247
|
+
if (selectedRow) selectedRow.classList.remove('selected');
|
|
248
|
+
if (row) { row.classList.add('selected'); selectedRow = row; }
|
|
249
|
+
renderMeta(w);
|
|
250
|
+
const doc = document.getElementById('doc');
|
|
251
|
+
setStatus(doc, 'Loading…', 'hint');
|
|
252
|
+
try {
|
|
253
|
+
const r = await fetch('/api/planning?path=' + encodeURIComponent(w.path));
|
|
254
|
+
if (!r.ok) {
|
|
255
|
+
const err = await r.json().catch(() => ({ error: 'unknown' }));
|
|
256
|
+
setStatus(doc, r.status + ': ' + (err.error || 'failed'), 'err');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const j = await r.json();
|
|
260
|
+
while (doc.firstChild) doc.removeChild(doc.firstChild);
|
|
261
|
+
doc.appendChild(renderMarkdownToFragment(j.body || ''));
|
|
262
|
+
} catch (e) {
|
|
263
|
+
setStatus(doc, e.message, 'err');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function loadWaves() {
|
|
268
|
+
const list = document.getElementById('waveList');
|
|
269
|
+
setStatus(list, 'Loading…', 'hint');
|
|
270
|
+
try {
|
|
271
|
+
const r = await fetch('/api/waves');
|
|
272
|
+
const j = await r.json();
|
|
273
|
+
while (list.firstChild) list.removeChild(list.firstChild);
|
|
274
|
+
const waves = j.waves || [];
|
|
275
|
+
if (waves.length === 0) {
|
|
276
|
+
setStatus(list, 'No waves found in .ijfw/wave-*/', 'hint');
|
|
277
|
+
setStatus(document.getElementById('doc'),
|
|
278
|
+
'No wave-* directories exist yet. Dispatch a wave via /superpowers:subagent-driven-development to see live state here.',
|
|
279
|
+
'hint');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
for (const w of waves) {
|
|
283
|
+
const row = document.createElement('div');
|
|
284
|
+
row.className = 'wave-row status-' + (w.status || 'unknown');
|
|
285
|
+
const id = document.createElement('div'); id.className = 'id'; id.textContent = w.id;
|
|
286
|
+
const status = document.createElement('div'); status.className = 'status'; status.textContent = w.status || 'unknown';
|
|
287
|
+
const ts = document.createElement('div'); ts.className = 'ts'; ts.textContent = w.checkpoint_at ?? (w.created_at ?? '');
|
|
288
|
+
row.appendChild(id); row.appendChild(status); row.appendChild(ts);
|
|
289
|
+
row.addEventListener('click', () => loadWaveState(w, row));
|
|
290
|
+
list.appendChild(row);
|
|
291
|
+
}
|
|
292
|
+
// Auto-select first (newest) wave.
|
|
293
|
+
const first = list.firstChild;
|
|
294
|
+
if (first) loadWaveState(waves[0], first);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
setStatus(list, e.message, 'err');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
setStatus(document.getElementById('doc'), 'Select a wave from the list to view its STATE.md.', 'hint');
|
|
301
|
+
loadWaves();
|
|
302
|
+
</script>
|
|
303
|
+
</body>
|
|
304
|
+
</html>
|
|
@@ -255,6 +255,10 @@ tr:hover td{background:var(--surface)}
|
|
|
255
255
|
<div class="breadcrumb" id="breadcrumb"><span style="color:var(--fg-dim)">Overview</span> <b>Today</b></div>
|
|
256
256
|
<div class="spacer"></div>
|
|
257
257
|
<span class="tier-pill">MAX 20x</span>
|
|
258
|
+
<!-- v1.5.0 N4.obs M5: cross-link back to the operator dashboard (port
|
|
259
|
+
19747). Operators flip between MCP (wave/orchestrator views) and
|
|
260
|
+
operator (cost/memory views) so the link is reciprocal. -->
|
|
261
|
+
<a class="icon-btn" id="operatorDashLink" href="http://localhost:19747/" target="_blank" rel="noopener" title="Open operator dashboard (cost + memory views)">Operator dashboard</a>
|
|
258
262
|
<button class="icon-btn" id="themeBtn" aria-label="Toggle theme">☾ Theme</button>
|
|
259
263
|
</header>
|
|
260
264
|
|
|
@@ -275,7 +279,7 @@ tr:hover td{background:var(--surface)}
|
|
|
275
279
|
<div class="hcard">
|
|
276
280
|
<div class="hlabel"><span class="pulse"></span> Active Session</div>
|
|
277
281
|
<div class="hval" style="font-size:20px;padding-top:6px">ijfw</div>
|
|
278
|
-
<div class="hsub">claude-opus-4-
|
|
282
|
+
<div class="hsub">claude-opus-4-7 -- <b>current</b></div>
|
|
279
283
|
</div>
|
|
280
284
|
<div class="hcard">
|
|
281
285
|
<div class="hlabel">Cache Efficiency</div>
|
package/src/dashboard-server.js
CHANGED
|
@@ -513,6 +513,79 @@ export async function startServer(options = {}) {
|
|
|
513
513
|
}
|
|
514
514
|
}],
|
|
515
515
|
|
|
516
|
+
// v1.5.0 S10 — wave-state JSON list. Reads .ijfw/wave-*/STATE.md frontmatter.
|
|
517
|
+
// Sorted by checkpoint_at desc, capped at 50 (any project with >50 active
|
|
518
|
+
// waves has bigger problems). Same security pattern as /api/planning.
|
|
519
|
+
['/api/waves', async (req, res) => {
|
|
520
|
+
try {
|
|
521
|
+
const ijfwDir = join(REPO_ROOT, '.ijfw');
|
|
522
|
+
if (!existsSync(ijfwDir)) {
|
|
523
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
524
|
+
res.end(JSON.stringify({ waves: [] }));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const entries = readdirSync(ijfwDir, { withFileTypes: true });
|
|
528
|
+
const { readWaveState } = await import('./orchestrator/wave-state.js');
|
|
529
|
+
const out = [];
|
|
530
|
+
for (const ent of entries) {
|
|
531
|
+
if (!ent.isDirectory() || !ent.name.startsWith('wave-')) continue;
|
|
532
|
+
const waveId = ent.name.slice('wave-'.length);
|
|
533
|
+
if (!waveId || !/^[A-Za-z0-9_-]+$/.test(waveId)) continue;
|
|
534
|
+
try {
|
|
535
|
+
const state = await readWaveState(waveId, REPO_ROOT);
|
|
536
|
+
if (!state) continue;
|
|
537
|
+
out.push({
|
|
538
|
+
id: waveId,
|
|
539
|
+
status: state.frontmatter.status ?? 'unknown',
|
|
540
|
+
created_at: state.frontmatter.created_at ?? null,
|
|
541
|
+
checkpoint_at: state.frontmatter.checkpoint_at ?? null,
|
|
542
|
+
claims_active: state.frontmatter.claims_active ?? 0,
|
|
543
|
+
agents_count: Array.isArray(state.frontmatter.agents) ? state.frontmatter.agents.length : 0,
|
|
544
|
+
path: `.ijfw/wave-${waveId}/STATE.md`,
|
|
545
|
+
});
|
|
546
|
+
} catch { /* skip malformed wave dirs */ }
|
|
547
|
+
}
|
|
548
|
+
out.sort((a, b) => String(b.checkpoint_at || '').localeCompare(String(a.checkpoint_at || '')));
|
|
549
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
550
|
+
res.end(JSON.stringify({ waves: out.slice(0, 50) }));
|
|
551
|
+
} catch (err) {
|
|
552
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
553
|
+
res.end(JSON.stringify({ error: err.message, endpoint: '/api/waves' }));
|
|
554
|
+
}
|
|
555
|
+
}],
|
|
556
|
+
|
|
557
|
+
// v1.5.0 S10 — wave-state viewer (HTML SPA). Same CSP as /planning.
|
|
558
|
+
['/waves', async (req, res) => {
|
|
559
|
+
try {
|
|
560
|
+
const html = await readFile(join(__dirname, 'dashboard-client-waves.html'), 'utf8');
|
|
561
|
+
res.writeHead(200, {
|
|
562
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
563
|
+
'Cache-Control': 'no-store',
|
|
564
|
+
'Content-Security-Policy': "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'",
|
|
565
|
+
});
|
|
566
|
+
res.end(html);
|
|
567
|
+
} catch (err) {
|
|
568
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
569
|
+
res.end('Waves viewer not found: ' + err.message);
|
|
570
|
+
}
|
|
571
|
+
}],
|
|
572
|
+
|
|
573
|
+
// v1.5.0 F4 — serve checkpoint-contract.md as plain text so operators can
|
|
574
|
+
// find the implementer-side checkpoint protocol from the dashboard.
|
|
575
|
+
['/docs/checkpoint-contract', async (req, res) => {
|
|
576
|
+
try {
|
|
577
|
+
const md = await readFile(join(__dirname, 'orchestrator/checkpoint-contract.md'), 'utf8');
|
|
578
|
+
res.writeHead(200, {
|
|
579
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
580
|
+
'Cache-Control': 'public, max-age=300',
|
|
581
|
+
});
|
|
582
|
+
res.end(md);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
585
|
+
res.end('Checkpoint contract not found: ' + err.message);
|
|
586
|
+
}
|
|
587
|
+
}],
|
|
588
|
+
|
|
516
589
|
['/api/memory/file', (req, res, url) => {
|
|
517
590
|
const rawPath = url.searchParams.get('path') || '';
|
|
518
591
|
if (!rawPath) {
|