@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.
- package/bin/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- 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 +6 -3
- 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 +754 -159
- package/src/cross-orchestrator.js +1065 -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/hardware-signer.js +4 -2
- 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 +595 -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 +267 -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/migration-runner.js +6 -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/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +152 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +46 -15
- 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-trigger.js +374 -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 +277 -0
- package/src/orchestrator/review.js +38 -3
- 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 +1932 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +471 -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-resolver.js +5 -3
- 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 +961 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -1
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +795 -112
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +102 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +44 -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 +44 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +440 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
- package/src/dashboard-charts.js +0 -239
|
@@ -14,6 +14,7 @@ import { homedir } from 'node:os';
|
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
15
|
import { writeAtomic } from './lib/atomic-io.js';
|
|
16
16
|
import { runCrossOp } from './cross-orchestrator.js';
|
|
17
|
+
import { chunkText, mergeFindings, CHUNKER_DEFAULTS } from './cross-audit-chunker.js';
|
|
17
18
|
import { readReceipts, purgeReceipts } from './receipts.js';
|
|
18
19
|
import { renderHeroLine } from './hero-line.js';
|
|
19
20
|
import { ROSTER, isInstalled, isReachable } from './audit-roster.js';
|
|
@@ -21,16 +22,27 @@ import { aggregatePortfolioFindings } from './cross-project-search.js';
|
|
|
21
22
|
import { runImport, runImportAll, listImporters } from './importers/cli.js';
|
|
22
23
|
import { validateToken } from './lib/token.js';
|
|
23
24
|
import { isVersionStringValid } from './lib/npm-view.js';
|
|
25
|
+
import { verifyShasumCrossSource } from './lib/shasum-verify.js';
|
|
24
26
|
import {
|
|
25
27
|
addBlackboardNote,
|
|
26
28
|
blackboardStatus,
|
|
27
29
|
claimArtifact,
|
|
30
|
+
DEFAULT_CLAIM_TTL_MS,
|
|
31
|
+
evictOrphanedClaims,
|
|
28
32
|
initBlackboard,
|
|
29
33
|
readBlackboard,
|
|
30
34
|
releaseClaim,
|
|
35
|
+
updateClaimHeartbeat,
|
|
31
36
|
writeHandoff,
|
|
32
37
|
} from './blackboard.js';
|
|
33
38
|
import { createTeamAssembly, readTeamAssembly } from './team/generator.js';
|
|
39
|
+
import {
|
|
40
|
+
addTeamRole,
|
|
41
|
+
checkTeamAssembly,
|
|
42
|
+
listTeamRoles,
|
|
43
|
+
removeTeamRole,
|
|
44
|
+
swapTeamRole,
|
|
45
|
+
} from './team/modify.js';
|
|
34
46
|
import {
|
|
35
47
|
blockSwarmTask,
|
|
36
48
|
buildSwarmPlan,
|
|
@@ -50,6 +62,8 @@ import {
|
|
|
50
62
|
} from './swarm/worktree.js';
|
|
51
63
|
import { renderSwarmDispatchPrompt } from './swarm/dispatch-prompt.js';
|
|
52
64
|
import { syncCodexAgents } from './codex-agents.js';
|
|
65
|
+
// v1.5.1 W2.H — memory benchmark harness (T22). Surfaced via `ijfw metrics --benchmark`.
|
|
66
|
+
import { runBenchmark } from './memory/benchmark.js';
|
|
53
67
|
import {
|
|
54
68
|
DESIGN_ACTIONS,
|
|
55
69
|
auditDesignText,
|
|
@@ -57,6 +71,12 @@ import {
|
|
|
57
71
|
initialDesignMarkdown,
|
|
58
72
|
loadDesignContext,
|
|
59
73
|
} from './design-intelligence.js';
|
|
74
|
+
// v1.5.1 W3.A.4 — registry-driven usage + alias help.
|
|
75
|
+
// SINGLE SOURCE OF TRUTH lives at installer/src/command-registry.js;
|
|
76
|
+
// import sibling-package style, matching extension-installer.js precedent.
|
|
77
|
+
import {
|
|
78
|
+
COMMAND_REGISTRY,
|
|
79
|
+
} from '../../installer/src/command-registry.js';
|
|
60
80
|
|
|
61
81
|
// ---------------------------------------------------------------------------
|
|
62
82
|
// Auditor error translator (1.2.5)
|
|
@@ -207,84 +227,37 @@ function parseArgs(argv) {
|
|
|
207
227
|
return out;
|
|
208
228
|
}
|
|
209
229
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
consolidate: {
|
|
224
|
-
title: 'IJFW consolidate',
|
|
225
|
-
usage: 'Use the ijfw-handoff or ijfw-memory-audit skill to consolidate decisions into memory. For swarm state, run: ijfw memory checkpoint <label>.',
|
|
226
|
-
},
|
|
227
|
-
'ijfw-audit': {
|
|
228
|
-
title: 'IJFW audit',
|
|
229
|
-
usage: 'Run verification with: ijfw preflight. For multi-model review, run: ijfw cross audit <target>.',
|
|
230
|
-
},
|
|
231
|
-
'ijfw-execute': {
|
|
232
|
-
title: 'IJFW execute',
|
|
233
|
-
usage: 'Use ijfw-workflow in agents, then terminal helpers: ijfw team init, ijfw swarm plan, ijfw swarm prepare, ijfw swarm start <task-id>.',
|
|
234
|
-
},
|
|
235
|
-
'ijfw-help': {
|
|
236
|
-
title: 'IJFW help',
|
|
237
|
-
usage: 'Run: ijfw help. Add --browser for the rendered local guide.',
|
|
238
|
-
},
|
|
239
|
-
'ijfw-plan': {
|
|
240
|
-
title: 'IJFW plan',
|
|
241
|
-
usage: 'Use ijfw-workflow for planning. Terminal helpers: ijfw team init, ijfw swarm plan, ijfw swarm prepare --reviews.',
|
|
242
|
-
},
|
|
243
|
-
'ijfw-ship': {
|
|
244
|
-
title: 'IJFW ship',
|
|
245
|
-
usage: 'Run: ijfw preflight. Do not publish or tag until your release gate is explicitly cleared.',
|
|
246
|
-
},
|
|
247
|
-
'ijfw-verify': {
|
|
248
|
-
title: 'IJFW verify',
|
|
249
|
-
usage: 'Run: ijfw preflight. For focused review, run: ijfw cross audit <target>.',
|
|
250
|
-
},
|
|
251
|
-
'memory-audit': {
|
|
252
|
-
title: 'IJFW memory audit',
|
|
253
|
-
usage: 'Use the ijfw-memory-audit skill in agents. Terminal safety net: ijfw recover status and ijfw memory checkpoint <label>.',
|
|
254
|
-
},
|
|
255
|
-
'memory-consent': {
|
|
256
|
-
title: 'IJFW memory consent',
|
|
257
|
-
usage: 'Use IJFW memory tools only for explicit project memory. Terminal checkpoint: ijfw memory checkpoint <label>.',
|
|
258
|
-
},
|
|
259
|
-
'memory-why': {
|
|
260
|
-
title: 'IJFW memory why',
|
|
261
|
-
usage: 'Use ijfw-recall or ijfw-memory-audit in agents to inspect why memory exists. Terminal recovery state: ijfw recover latest.',
|
|
262
|
-
},
|
|
263
|
-
metrics: {
|
|
264
|
-
title: 'IJFW metrics',
|
|
265
|
-
usage: 'Open the dashboard with: ijfw dashboard start. Agent-side metrics are available through ijfw_metrics.',
|
|
266
|
-
},
|
|
267
|
-
mode: {
|
|
268
|
-
title: 'IJFW mode',
|
|
269
|
-
usage: 'Inspect configuration with: ijfw config --audit. Statusline mode helpers: ijfw statusline --status, --compose, or --disable.',
|
|
270
|
-
},
|
|
271
|
-
};
|
|
230
|
+
// v1.5.1 W3.A.4 — COMMAND_ALIAS_HELP is now derived from the command-registry
|
|
231
|
+
// (entries where tier === 'pointer-stub'). Each entry's deprecatedReason
|
|
232
|
+
// becomes the usage line; title is auto-derived from the canonical name.
|
|
233
|
+
// To change the help text or add a new pointer-stub: edit
|
|
234
|
+
// installer/src/command-registry.js.
|
|
235
|
+
const COMMAND_ALIAS_HELP = Object.freeze(Object.fromEntries(
|
|
236
|
+
COMMAND_REGISTRY
|
|
237
|
+
.filter(e => e.tier === 'pointer-stub')
|
|
238
|
+
.map(e => [e.name, {
|
|
239
|
+
title: `IJFW ${e.name}`,
|
|
240
|
+
usage: e.deprecatedReason,
|
|
241
|
+
}])
|
|
242
|
+
));
|
|
272
243
|
|
|
273
244
|
function parseCrossAlias(mode, args) {
|
|
274
245
|
let only = null;
|
|
275
246
|
let confirm = false;
|
|
276
247
|
let expand = false;
|
|
248
|
+
let chunk = false;
|
|
277
249
|
const positional = [];
|
|
278
250
|
for (let i = 1; i < args.length; i++) {
|
|
279
251
|
const arg = args[i];
|
|
280
252
|
if (arg === '--confirm') confirm = true;
|
|
281
253
|
else if (arg === '--expand') expand = true;
|
|
254
|
+
else if (arg === '--chunk') chunk = true; // v1.5.1 H1.6 — wire chunker
|
|
282
255
|
else if (arg === '--with' && args[i + 1]) only = args[++i];
|
|
283
256
|
else if (arg.startsWith('--with=')) only = arg.slice('--with='.length);
|
|
284
257
|
else if (!arg.startsWith('--')) positional.push(arg);
|
|
285
258
|
}
|
|
286
259
|
const target = mode === 'research' ? positional.join(' ').trim() : positional[0];
|
|
287
|
-
return { cmd: 'cross', mode, target: target || undefined, only, confirm, expand };
|
|
260
|
+
return { cmd: 'cross', mode, target: target || undefined, only, confirm, expand, chunk };
|
|
288
261
|
}
|
|
289
262
|
|
|
290
263
|
function parseCommandAlias(args) {
|
|
@@ -299,6 +272,16 @@ function parseCommandAlias(args) {
|
|
|
299
272
|
return parseCrossAlias('research', args);
|
|
300
273
|
}
|
|
301
274
|
if (Object.prototype.hasOwnProperty.call(COMMAND_ALIAS_HELP, name)) {
|
|
275
|
+
// v1.5.1 W2.H — `ijfw metrics` is a deprecated pointer-stub, but
|
|
276
|
+
// `ijfw metrics --benchmark` surfaces the memory benchmark harness (T22).
|
|
277
|
+
// Bare `ijfw metrics` still falls through to the deprecation redirect.
|
|
278
|
+
if (name === 'metrics' && args.includes('--benchmark')) {
|
|
279
|
+
const opts = { cmd: 'metrics-benchmark', json: args.includes('--json'), write: true };
|
|
280
|
+
for (let i = 1; i < args.length; i++) {
|
|
281
|
+
if (args[i] === '--no-write') opts.write = false;
|
|
282
|
+
}
|
|
283
|
+
return opts;
|
|
284
|
+
}
|
|
302
285
|
return { cmd: 'command-alias', alias: name };
|
|
303
286
|
}
|
|
304
287
|
return null;
|
|
@@ -396,6 +379,23 @@ function parseArgsInner(args) {
|
|
|
396
379
|
return { cmd: 'design', sub: args[1] || 'status' };
|
|
397
380
|
}
|
|
398
381
|
|
|
382
|
+
// v1.5.0 wire-W1.D — `ijfw ui-review --spec <path> --scope <dirs>`
|
|
383
|
+
if (args[0] === 'ui-review') {
|
|
384
|
+
const opts = { cmd: 'ui-review', spec: null, scope: null, write: true, gcSketches: false, peerInputs: null };
|
|
385
|
+
for (let i = 1; i < args.length; i++) {
|
|
386
|
+
const a = args[i];
|
|
387
|
+
if ((a === '--spec' || a === '-s') && args[i + 1]) { opts.spec = args[++i]; }
|
|
388
|
+
else if (a.startsWith('--spec=')) opts.spec = a.slice('--spec='.length);
|
|
389
|
+
else if ((a === '--scope' || a === '-S') && args[i + 1]) { opts.scope = args[++i]; }
|
|
390
|
+
else if (a.startsWith('--scope=')) opts.scope = a.slice('--scope='.length);
|
|
391
|
+
else if (a === '--no-write') opts.write = false;
|
|
392
|
+
else if (a === '--gc-sketches') opts.gcSketches = true;
|
|
393
|
+
else if ((a === '--peer-inputs') && args[i + 1]) { opts.peerInputs = args[++i]; }
|
|
394
|
+
else if (a.startsWith('--peer-inputs=')) opts.peerInputs = a.slice('--peer-inputs='.length);
|
|
395
|
+
}
|
|
396
|
+
return opts;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
399
|
if (args[0] === 'blackboard') {
|
|
400
400
|
return { cmd: 'blackboard', sub: args[1] || 'status' };
|
|
401
401
|
}
|
|
@@ -418,8 +418,25 @@ function parseArgsInner(args) {
|
|
|
418
418
|
return { cmd: 'codex', sub: args[1] || 'doctor' };
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
-
if (args[0] === 'memory'
|
|
422
|
-
|
|
421
|
+
if (args[0] === 'memory') {
|
|
422
|
+
// `ijfw memory` / `ijfw memory --help` / `ijfw memory -h` → namespace help
|
|
423
|
+
if (args.length === 1 || args[1] === '--help' || args[1] === '-h') {
|
|
424
|
+
return { cmd: 'memory-help' };
|
|
425
|
+
}
|
|
426
|
+
if (args[1] === 'checkpoint') {
|
|
427
|
+
return { cmd: 'memory-checkpoint', label: args[2] || 'manual' };
|
|
428
|
+
}
|
|
429
|
+
if (args[1] === 'reindex') {
|
|
430
|
+
// `ijfw memory reindex` -> M1 backfill (free, obsidian indexing)
|
|
431
|
+
// `ijfw memory reindex --m2` -> also run M2 A-Mem auto-link backfill
|
|
432
|
+
// (budget-gated; needs IJFW_AUTOLINK_*)
|
|
433
|
+
let m2 = false;
|
|
434
|
+
for (let i = 2; i < args.length; i++) {
|
|
435
|
+
if (args[i] === '--m2' || args[i] === '--autolink') m2 = true;
|
|
436
|
+
}
|
|
437
|
+
return { cmd: 'memory-reindex', m2 };
|
|
438
|
+
}
|
|
439
|
+
return { cmd: 'memory-unknown', sub: args[1] };
|
|
423
440
|
}
|
|
424
441
|
|
|
425
442
|
if (args[0] === 'recover') {
|
|
@@ -464,14 +481,16 @@ function parseArgsInner(args) {
|
|
|
464
481
|
let only = null;
|
|
465
482
|
let confirm = false;
|
|
466
483
|
let expand = false;
|
|
484
|
+
let chunk = false;
|
|
467
485
|
|
|
468
486
|
for (let i = 3; i < args.length; i++) {
|
|
469
487
|
if (args[i] === '--confirm') { confirm = true; }
|
|
470
488
|
else if (args[i] === '--expand') { expand = true; }
|
|
489
|
+
else if (args[i] === '--chunk') { chunk = true; } // v1.5.1 H1.6 — wire chunker
|
|
471
490
|
else if (args[i] === '--with' && args[i + 1]) { only = args[++i]; }
|
|
472
491
|
}
|
|
473
492
|
|
|
474
|
-
return { cmd: 'cross', mode, target, only, confirm, expand };
|
|
493
|
+
return { cmd: 'cross', mode, target, only, confirm, expand, chunk };
|
|
475
494
|
}
|
|
476
495
|
|
|
477
496
|
return { cmd: 'unknown', raw: args[0] };
|
|
@@ -481,86 +500,32 @@ function parseArgsInner(args) {
|
|
|
481
500
|
// Commands
|
|
482
501
|
// ---------------------------------------------------------------------------
|
|
483
502
|
|
|
484
|
-
function
|
|
503
|
+
function printMemoryHelp() {
|
|
485
504
|
console.log(`
|
|
486
|
-
ijfw --
|
|
487
|
-
Fire 2-4 AIs at any target. Receipts logged. Cache hits tracked. Memory follows you.
|
|
505
|
+
ijfw memory -- project memory namespace
|
|
488
506
|
|
|
489
507
|
Usage:
|
|
490
|
-
ijfw
|
|
491
|
-
|
|
492
|
-
ijfw
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
ijfw
|
|
501
|
-
ijfw
|
|
502
|
-
ijfw
|
|
503
|
-
ijfw cross project-audit <rule-file> [--dry-run]
|
|
504
|
-
ijfw import <tool> [--all] [--dry-run] [--force] [--path <p>]
|
|
505
|
-
ijfw status
|
|
506
|
-
ijfw doctor
|
|
507
|
-
ijfw update
|
|
508
|
-
ijfw receipt last
|
|
509
|
-
ijfw --purge-receipts
|
|
510
|
-
ijfw --help
|
|
511
|
-
|
|
512
|
-
Commands:
|
|
513
|
-
install Install IJFW into your AI coding agents.
|
|
514
|
-
uninstall Remove IJFW and revert AI-agent configs. Same as: ijfw off
|
|
515
|
-
preflight Run the 11-gate quality pipeline (blocking + advisory).
|
|
516
|
-
dashboard Control the dashboard server (start, stop, status).
|
|
517
|
-
design Control the live visual design companion.
|
|
518
|
-
blackboard Coordinate project-local swarm state and artifact claims.
|
|
519
|
-
team Assemble project agents, charter, and workflow manifest.
|
|
520
|
-
swarm Plan artifact-aware parallel work from the team manifest.
|
|
521
|
-
recover Show the latest checkpoint and next recovery step.
|
|
522
|
-
demo 30-second live tour of the Trident (fires real auditors).
|
|
523
|
-
cross Fire external auditors at a target. Try: ijfw cross audit README.md
|
|
524
|
-
import Pull memory in from another tool. Try: ijfw import claude-mem --all
|
|
525
|
-
status Show recent cross-audit activity. Try: ijfw status
|
|
526
|
-
doctor Probe which CLIs and API keys are reachable. Try: ijfw doctor
|
|
527
|
-
update Pull latest IJFW + reinstall merge-safely. Try: ijfw update
|
|
528
|
-
update --check Non-invasive check. Exits 0 always; prints "update-available: <ver>" when an update exists (grep-safe).
|
|
529
|
-
receipt last Print a redacted, shareable block from the last Trident run.
|
|
530
|
-
--purge-receipts Clear the cross-runs receipt log. Try: ijfw --purge-receipts
|
|
531
|
-
|
|
532
|
-
Modes (for ijfw cross):
|
|
533
|
-
audit Adversarial review of a file, module, or path
|
|
534
|
-
research Multi-source research on a topic
|
|
535
|
-
critique Structured counter-argument generation
|
|
536
|
-
project-audit Run the same audit across every registered IJFW project
|
|
537
|
-
Usage: ijfw cross project-audit <rule-file> [--dry-run]
|
|
538
|
-
|
|
539
|
-
Options for ijfw cross:
|
|
540
|
-
--with <id> Force a specific auditor (comma-separated for multiple)
|
|
541
|
-
--confirm Prompt for confirmation before firing
|
|
542
|
-
--expand Include extended swarm when available
|
|
543
|
-
|
|
544
|
-
Global flags:
|
|
545
|
-
--json Emit JSON instead of human output. status and doctor auto-JSON
|
|
546
|
-
on non-TTY (gh-CLI convention); version stays one-line on pipe
|
|
547
|
-
and only JSON-ifies with explicit --json. Other commands ignore.
|
|
548
|
-
|
|
549
|
-
Environment:
|
|
550
|
-
IJFW_AUDIT_BUDGET_USD Session spend cap (default $2.00). First call is always
|
|
551
|
-
allowed (no cap). Cap enforced from the 2nd call on.
|
|
552
|
-
|
|
553
|
-
Examples:
|
|
554
|
-
ijfw demo
|
|
555
|
-
ijfw cross audit README.md
|
|
556
|
-
ijfw cross research "vector search approaches"
|
|
557
|
-
ijfw cross critique HEAD~3..HEAD
|
|
558
|
-
ijfw cross audit CLAUDE.md --with codex,gemini
|
|
559
|
-
ijfw status
|
|
560
|
-
ijfw doctor
|
|
508
|
+
ijfw memory checkpoint <label> Snapshot current swarm/memory state under <label>.
|
|
509
|
+
<label> defaults to "manual" if omitted.
|
|
510
|
+
ijfw memory reindex [--m2] Backfill M1 obsidian indexing (wikilinks,
|
|
511
|
+
#tags, [k:: v] metadata) over the whole
|
|
512
|
+
memory db. Free + idempotent. Add --m2 to
|
|
513
|
+
also run the A-Mem auto-link backfill --
|
|
514
|
+
budget-gated (set IJFW_AUTOLINK_BUDGET_USD
|
|
515
|
+
and IJFW_AUTOLINK_BACKFILL=1).
|
|
516
|
+
|
|
517
|
+
Related:
|
|
518
|
+
ijfw recover [status|latest] Inspect checkpoints and recovery state.
|
|
519
|
+
ijfw --help Top-level user-facing commands.
|
|
520
|
+
ijfw commands Full command surface (all verbs).
|
|
561
521
|
`.trim());
|
|
562
522
|
}
|
|
563
523
|
|
|
524
|
+
function printUnknownCommand(raw) {
|
|
525
|
+
console.error(`Unknown command: ${raw}`);
|
|
526
|
+
console.error('Run `ijfw --help` for the user-facing command list, or `ijfw commands` for the full surface.');
|
|
527
|
+
}
|
|
528
|
+
|
|
564
529
|
function cmdCommandAlias(alias) {
|
|
565
530
|
const info = COMMAND_ALIAS_HELP[alias];
|
|
566
531
|
if (!info) {
|
|
@@ -573,6 +538,41 @@ function cmdCommandAlias(alias) {
|
|
|
573
538
|
process.exit(0);
|
|
574
539
|
}
|
|
575
540
|
|
|
541
|
+
// v1.5.1 W2.H — `ijfw metrics --benchmark`: run the memory benchmark harness
|
|
542
|
+
// (T22) against IJFW's own 3-tier store and report recall@k, MRR/NDCG-style
|
|
543
|
+
// retrieval quality, throughput, and p50/p95/p99 latency. See
|
|
544
|
+
// docs/MEMORY-BENCHMARK.md for the axes and how to interpret the numbers.
|
|
545
|
+
async function cmdMetricsBenchmark(opts = {}) {
|
|
546
|
+
const results = await runBenchmark({
|
|
547
|
+
root: process.cwd(),
|
|
548
|
+
write: opts.write !== false,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
if (wantsJson(opts)) {
|
|
552
|
+
emitJson(results);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const q = results.axes.query_warm_fts5;
|
|
557
|
+
const ing = results.axes.ingest;
|
|
558
|
+
const recallPairs = Object.entries(q.recall || {});
|
|
559
|
+
console.log('IJFW memory benchmark (T22)');
|
|
560
|
+
console.log('');
|
|
561
|
+
console.log(` corpus ${results.corpus.docs} docs / ${results.corpus.queries} queries / ${results.corpus.total_query_samples} timed samples`);
|
|
562
|
+
console.log(` ingest throughput ${ing.throughput_rps} rows/s`);
|
|
563
|
+
console.log(` ingest latency p50 ${ing.latency_ms.p50}ms p95 ${ing.latency_ms.p95}ms p99 ${ing.latency_ms.p99}ms`);
|
|
564
|
+
console.log(` query latency p50 ${q.latency_ms.p50}ms p95 ${q.latency_ms.p95}ms p99 ${q.latency_ms.p99}ms`);
|
|
565
|
+
console.log(` recall ${recallPairs.map(([k, v]) => `${k}=${v.toFixed(3)}`).join(' ')}`);
|
|
566
|
+
console.log(` storage ${results.axes.storage.bytes_per_memory} bytes/memory (${results.axes.storage.rows_indexed} rows)`);
|
|
567
|
+
console.log(` cold tier ${results.axes.query_cold_vector.available ? 'available' : 'reserved (no embedding model)'}`);
|
|
568
|
+
if (results.artifact_path) {
|
|
569
|
+
console.log('');
|
|
570
|
+
console.log(` artifact ${results.artifact_path}`);
|
|
571
|
+
}
|
|
572
|
+
console.log('');
|
|
573
|
+
console.log('Axes explained: docs/MEMORY-BENCHMARK.md');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
576
|
async function cmdStatus(projectDir, opts = {}) {
|
|
577
577
|
const receipts = readReceipts(projectDir);
|
|
578
578
|
const last = receipts[receipts.length - 1];
|
|
@@ -663,8 +663,6 @@ async function cmdDemo() {
|
|
|
663
663
|
|
|
664
664
|
let result;
|
|
665
665
|
try {
|
|
666
|
-
// TODO post-merge: perAuditorTimeoutSec, minResponses, quiet are added by Item 2 agent.
|
|
667
|
-
// Passed through here; current orchestrator silently ignores unknown params.
|
|
668
666
|
result = await runCrossOp({
|
|
669
667
|
mode: 'audit',
|
|
670
668
|
target,
|
|
@@ -877,6 +875,15 @@ function cmdPurgeReceipts(projectDir) {
|
|
|
877
875
|
// ranges, and non-existent paths pass through unchanged.
|
|
878
876
|
const TARGET_FILE_SIZE_CAP = 64 * 1024; // 64 KB -- leaves prompt headroom
|
|
879
877
|
|
|
878
|
+
// r17.1 — size thresholds for pre-flight advisory. Inputs under WARN are
|
|
879
|
+
// silent; WARN..CAP get a one-line "this might be slow" advisory; CAP..MAX
|
|
880
|
+
// get a "this WILL be truncated, consider chunking" warning before we fire
|
|
881
|
+
// any auditor (so the user can cancel rather than burn wall time). Anything
|
|
882
|
+
// over MAX would be silently truncated by resolveTarget anyway — the
|
|
883
|
+
// pre-flight check makes that loud.
|
|
884
|
+
const TARGET_FILE_SIZE_WARN = 32 * 1024;
|
|
885
|
+
const TARGET_FILE_SIZE_MAX = 256 * 1024; // beyond this, advise chunking explicitly
|
|
886
|
+
|
|
880
887
|
export function resolveTarget(raw, opts = {}) {
|
|
881
888
|
const cap = typeof opts.sizeCap === 'number' ? opts.sizeCap : TARGET_FILE_SIZE_CAP;
|
|
882
889
|
if (typeof raw !== 'string' || !raw) return raw;
|
|
@@ -921,7 +928,44 @@ export function resolveTarget(raw, opts = {}) {
|
|
|
921
928
|
return `File: ${raw}\n\n${contents}`;
|
|
922
929
|
}
|
|
923
930
|
|
|
924
|
-
|
|
931
|
+
// v1.5.1 H1.6 — chunked-dispatch helpers (audit finding token-optimization.md
|
|
932
|
+
// HIGH-H4 + trident.md HIGH-1, 2/2 consensus). The chunker has shipped (with
|
|
933
|
+
// tests) since r17.1 but was never wired into the CLI. `--chunk` now triggers
|
|
934
|
+
// per-chunk dispatch through runCrossOp + a final Jaccard-dedupe merge.
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Decide whether a target absolute path is large enough to benefit from
|
|
938
|
+
* chunking. Exported for unit-tests.
|
|
939
|
+
*/
|
|
940
|
+
export function shouldChunkFile(absPath, opts = {}) {
|
|
941
|
+
const threshold = typeof opts.threshold === 'number' ? opts.threshold : TARGET_FILE_SIZE_CAP;
|
|
942
|
+
try {
|
|
943
|
+
const st = statSync(absPath);
|
|
944
|
+
if (!st.isFile()) return false;
|
|
945
|
+
return st.size > threshold;
|
|
946
|
+
} catch {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Read a file and produce the per-chunk target strings the auditors will see.
|
|
953
|
+
* Each chunk is annotated with its index so cross-chunk findings can be
|
|
954
|
+
* reconciled. Exported for unit-tests.
|
|
955
|
+
*/
|
|
956
|
+
export function buildChunkedTargets(absPath, rawTarget, opts = {}) {
|
|
957
|
+
const content = readFileSync(absPath, 'utf8');
|
|
958
|
+
const chunks = chunkText(content, opts);
|
|
959
|
+
return chunks.map((c, i) => ({
|
|
960
|
+
chunkIndex: i,
|
|
961
|
+
total: chunks.length,
|
|
962
|
+
bytesStart: c.start,
|
|
963
|
+
bytesEnd: c.end,
|
|
964
|
+
target: `File: ${rawTarget} [chunk ${i + 1}/${chunks.length}, bytes ${c.start}-${c.end}]\n\n${c.text}`,
|
|
965
|
+
}));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async function cmdCross({ mode, target, only, confirm, expand, chunk }) {
|
|
925
969
|
const VALID_MODES = ['audit', 'research', 'critique'];
|
|
926
970
|
if (!mode || !VALID_MODES.includes(mode)) {
|
|
927
971
|
console.error(`ijfw cross requires a mode: ${VALID_MODES.join(', ')}. Example: ijfw cross audit <file>`);
|
|
@@ -935,6 +979,114 @@ async function cmdCross({ mode, target, only, confirm, expand }) {
|
|
|
935
979
|
// Issue #6 fix: substitute file contents for path string when target is a
|
|
936
980
|
// regular file. Keep the raw target for the user-facing echo line.
|
|
937
981
|
const rawTarget = target;
|
|
982
|
+
|
|
983
|
+
// r17.1 — pre-flight size advisory. Run BEFORE resolveTarget truncates,
|
|
984
|
+
// so the user sees the real number and can decide to abort + chunk the
|
|
985
|
+
// input themselves rather than getting a silently-truncated audit.
|
|
986
|
+
// r17-M3: resolve relative paths against cwd FIRST, matching the same
|
|
987
|
+
// resolution resolveTarget() uses. Without this, `ijfw cross audit foo.md`
|
|
988
|
+
// (a relative path that exists) would skip the advisory because
|
|
989
|
+
// existsSync(rawTarget) probes against the wrong cwd-anchor.
|
|
990
|
+
try {
|
|
991
|
+
let probePath = null;
|
|
992
|
+
if (typeof rawTarget === 'string' && rawTarget.length < 4096) {
|
|
993
|
+
const resolved = isAbsolute(rawTarget) ? rawTarget : resolve(process.cwd(), rawTarget);
|
|
994
|
+
if (existsSync(resolved)) probePath = resolved;
|
|
995
|
+
}
|
|
996
|
+
if (probePath) {
|
|
997
|
+
const st = statSync(probePath);
|
|
998
|
+
if (st.isFile()) {
|
|
999
|
+
if (st.size > TARGET_FILE_SIZE_MAX) {
|
|
1000
|
+
console.log('');
|
|
1001
|
+
console.log(`Heads up -- target is ${(st.size / 1024).toFixed(1)} KB, larger than the ${(TARGET_FILE_SIZE_MAX / 1024).toFixed(0)} KB recommended max.`);
|
|
1002
|
+
console.log(`Auditors will see only the first ${(TARGET_FILE_SIZE_CAP / 1024).toFixed(0)} KB (truncated). For full coverage, chunk the target into smaller files and audit each, OR pass --chunk to auto-split (see \`ijfw cross --help\`).`);
|
|
1003
|
+
} else if (st.size > TARGET_FILE_SIZE_CAP) {
|
|
1004
|
+
console.log('');
|
|
1005
|
+
console.log(`Note: target is ${(st.size / 1024).toFixed(1)} KB; auditors will see the first ${(TARGET_FILE_SIZE_CAP / 1024).toFixed(0)} KB and a truncation marker. Findings beyond that point will be missed.`);
|
|
1006
|
+
} else if (st.size > TARGET_FILE_SIZE_WARN) {
|
|
1007
|
+
console.log('');
|
|
1008
|
+
console.log(`Note: target is ${(st.size / 1024).toFixed(1)} KB; expect a slower wall time (gemini in particular may push the 90s budget).`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
} catch { /* statSync failure is non-fatal; size advisory is best-effort */ }
|
|
1013
|
+
|
|
1014
|
+
// v1.5.1 H1.6 — chunked dispatch path. When --chunk is set AND the target
|
|
1015
|
+
// is a file larger than the size cap, split via the cross-audit-chunker
|
|
1016
|
+
// (boundary-aware, 10% overlap, Jaccard-dedupe merge) and dispatch each
|
|
1017
|
+
// chunk through runCrossOp separately. Merge findings at the end. Opt-in
|
|
1018
|
+
// because cost scales linearly with chunk count.
|
|
1019
|
+
if (chunk) {
|
|
1020
|
+
let absPath = null;
|
|
1021
|
+
try {
|
|
1022
|
+
if (typeof rawTarget === 'string' && rawTarget.length < 4096) {
|
|
1023
|
+
const resolved = isAbsolute(rawTarget) ? rawTarget : resolve(process.cwd(), rawTarget);
|
|
1024
|
+
if (existsSync(resolved) && statSync(resolved).isFile()) absPath = resolved;
|
|
1025
|
+
}
|
|
1026
|
+
} catch { /* */ }
|
|
1027
|
+
|
|
1028
|
+
if (!absPath) {
|
|
1029
|
+
console.log('');
|
|
1030
|
+
console.log('--chunk requires a file target. Topics, git ranges, and missing paths cannot be chunked.');
|
|
1031
|
+
// Fall through to normal path -- --chunk silently no-ops for non-files
|
|
1032
|
+
} else if (!shouldChunkFile(absPath)) {
|
|
1033
|
+
console.log('');
|
|
1034
|
+
console.log(`--chunk: target is ${(statSync(absPath).size / 1024).toFixed(1)} KB, under the ${(TARGET_FILE_SIZE_CAP / 1024).toFixed(0)} KB chunk threshold; running single-pass audit instead.`);
|
|
1035
|
+
// Fall through to normal path
|
|
1036
|
+
} else {
|
|
1037
|
+
const chunks = buildChunkedTargets(absPath, rawTarget);
|
|
1038
|
+
console.log('');
|
|
1039
|
+
console.log(`--chunk: splitting ${rawTarget} into ${chunks.length} chunks (≈${(CHUNKER_DEFAULTS.chunkSize / 1024).toFixed(0)} KB each, ${(CHUNKER_DEFAULTS.overlap / 1024).toFixed(0)} KB overlap).`);
|
|
1040
|
+
console.log(`Trident dispatches: ${chunks.length} × per-chunk audit. Cost scales linearly.`);
|
|
1041
|
+
|
|
1042
|
+
const perChunkResults = [];
|
|
1043
|
+
const auditorIds = new Set();
|
|
1044
|
+
const projectDir = process.cwd();
|
|
1045
|
+
let firedAny = false;
|
|
1046
|
+
for (const { chunkIndex, total, target: chunkTarget } of chunks) {
|
|
1047
|
+
console.log('');
|
|
1048
|
+
console.log(`[chunk ${chunkIndex + 1}/${total}] dispatching...`);
|
|
1049
|
+
try {
|
|
1050
|
+
const r = await runCrossOp({
|
|
1051
|
+
mode, target: chunkTarget, projectDir,
|
|
1052
|
+
runStamp: new Date().toISOString(), only, confirm, expand,
|
|
1053
|
+
});
|
|
1054
|
+
const findings = Array.isArray(r.merged) ? r.merged : [];
|
|
1055
|
+
perChunkResults.push({ chunkIndex, findings });
|
|
1056
|
+
for (const p of (r.picks || [])) auditorIds.add(p.id);
|
|
1057
|
+
if ((r.picks || []).length > 0) firedAny = true;
|
|
1058
|
+
console.log(`[chunk ${chunkIndex + 1}/${total}] ${findings.length} finding(s) from ${(r.picks || []).length} auditor(s).`);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
console.log(`[chunk ${chunkIndex + 1}/${total}] dispatch error: ${err.message}`);
|
|
1061
|
+
perChunkResults.push({ chunkIndex, findings: [] });
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const merged = mergeFindings(perChunkResults);
|
|
1066
|
+
console.log('');
|
|
1067
|
+
console.log(`=== Chunked audit complete: ${merged.length} unique finding(s) across ${chunks.length} chunks ===`);
|
|
1068
|
+
console.log(`Auditors fired (union): ${[...auditorIds].join(', ') || '(none)'}`);
|
|
1069
|
+
if (!firedAny) {
|
|
1070
|
+
console.log('No auditors fired -- run `ijfw doctor` to see the install hints.');
|
|
1071
|
+
process.exit(2); // r17.1 — degraded exit code
|
|
1072
|
+
}
|
|
1073
|
+
for (const f of merged) {
|
|
1074
|
+
const sev = (f.severity || 'note').toUpperCase();
|
|
1075
|
+
const cluster = f.clusterSize > 1 ? ` [×${f.clusterSize}]` : '';
|
|
1076
|
+
const tgt = f.target ? ` ${f.target} —` : '';
|
|
1077
|
+
// v1.5.0 wire-W4: widen field fallback to cover description/issue/
|
|
1078
|
+
// detail/note/summary keys auditors emit. Closes the r19 "(no detail)"
|
|
1079
|
+
// dropout that made adjudication a guessing game.
|
|
1080
|
+
const text = f.finding || f.text || f.message ||
|
|
1081
|
+
f.description || f.issue ||
|
|
1082
|
+
f.detail || f.details || f.note || f.summary ||
|
|
1083
|
+
'(no detail)';
|
|
1084
|
+
console.log(` ${sev}${cluster}${tgt} ${text}`);
|
|
1085
|
+
}
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
938
1090
|
target = resolveTarget(target);
|
|
939
1091
|
|
|
940
1092
|
// Polish 6: pre-flight reachability check. If no auditor is wired, give a
|
|
@@ -1006,6 +1158,18 @@ async function cmdCross({ mode, target, only, confirm, expand }) {
|
|
|
1006
1158
|
printFindings(mode, merged);
|
|
1007
1159
|
|
|
1008
1160
|
console.log('\nReceipt logged -- run `ijfw status` to see it.');
|
|
1161
|
+
|
|
1162
|
+
// r17.1 — structured exit code so CI scripts + orchestrator-LLM callers
|
|
1163
|
+
// can detect degraded runs without scraping console output.
|
|
1164
|
+
// exit 0 — all picks contributed productively
|
|
1165
|
+
// exit 2 — at least one pick didn't contribute (timeout / failure / aborted)
|
|
1166
|
+
// exit 3 — zero picks contributed productively (INCONCLUSIVE verdict)
|
|
1167
|
+
// exit 1 is reserved for argv / usage errors (already used above).
|
|
1168
|
+
if (auditorResults && Array.isArray(auditorResults)) {
|
|
1169
|
+
const productive = auditorResults.filter(r => r.counted === true || r.status === null || r.status === 'fallback-used');
|
|
1170
|
+
if (productive.length === 0) process.exit(3);
|
|
1171
|
+
if (productive.length < auditorResults.length) process.exit(2);
|
|
1172
|
+
}
|
|
1009
1173
|
}
|
|
1010
1174
|
|
|
1011
1175
|
// ---------------------------------------------------------------------------
|
|
@@ -1314,16 +1478,100 @@ function cmpSemver(a, b) {
|
|
|
1314
1478
|
|
|
1315
1479
|
function readState() { return readJsonSafe(join(ijfwHome(), 'state.json')) || {}; }
|
|
1316
1480
|
function readSettings() { return readJsonSafe(join(ijfwHome(), 'settings.json')) || {}; }
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1481
|
+
|
|
1482
|
+
// v1.5.0 audit M11 (F-REL-1): writeStateFields was best-effort — readState +
|
|
1483
|
+
// merge + writeAtomic was a TOCTOU window where a parallel `ijfw update`
|
|
1484
|
+
// completion could clobber another writer's `last_applied_version`. If the
|
|
1485
|
+
// write failed silently, the re-entrancy guard (last_applied_version >=
|
|
1486
|
+
// last_latest_seen) would never fire and every subsequent session would
|
|
1487
|
+
// nag-loop until manual `ijfw doctor`.
|
|
1488
|
+
//
|
|
1489
|
+
// Fix: serialise read-modify-write under a sync directory lock so the merge
|
|
1490
|
+
// happens against the latest disk state. We use a tiny inlined sync version
|
|
1491
|
+
// of `withFsLock` (mkdir-with-EEXIST is atomic on POSIX + NTFS) rather than
|
|
1492
|
+
// importing the async `fs-lock.js` — this whole CLI is sync top-to-bottom and
|
|
1493
|
+
// converting just-this-callsite to async would force every caller to await.
|
|
1494
|
+
//
|
|
1495
|
+
// On lock-acquire failure we still attempt the write (better than refusing to
|
|
1496
|
+
// persist) and surface the lock failure as a clearer error.
|
|
1497
|
+
const STATE_LOCK_DIR = () => join(ijfwHome(), '.state.lock');
|
|
1498
|
+
const STATE_LOCK_ACQUIRE_TIMEOUT_MS = 5000;
|
|
1499
|
+
const STATE_LOCK_STALE_MS = 30000;
|
|
1500
|
+
const STATE_LOCK_BACKOFF_START_MS = 25;
|
|
1501
|
+
const STATE_LOCK_BACKOFF_MAX_MS = 250;
|
|
1502
|
+
|
|
1503
|
+
function withStateLockSync(fn) {
|
|
1504
|
+
const lockDir = STATE_LOCK_DIR();
|
|
1505
|
+
// Ensure parent exists; tolerate races.
|
|
1506
|
+
try { mkdirSync(dirname(lockDir), { recursive: true, mode: 0o700 }); } catch { /* */ }
|
|
1507
|
+
|
|
1508
|
+
const deadline = Date.now() + STATE_LOCK_ACQUIRE_TIMEOUT_MS;
|
|
1509
|
+
let staleRecoveryUsed = false;
|
|
1510
|
+
let backoff = STATE_LOCK_BACKOFF_START_MS;
|
|
1511
|
+
let acquired = false;
|
|
1512
|
+
|
|
1513
|
+
while (Date.now() < deadline) {
|
|
1514
|
+
try {
|
|
1515
|
+
mkdirSync(lockDir, { recursive: false });
|
|
1516
|
+
acquired = true;
|
|
1517
|
+
break;
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
if (err && err.code !== 'EEXIST') {
|
|
1520
|
+
// Real FS error (EACCES, ENOENT on parent we couldn't create) —
|
|
1521
|
+
// surface so the caller can fall back to best-effort write.
|
|
1522
|
+
throw err;
|
|
1523
|
+
}
|
|
1524
|
+
// Stale recovery: if the lock dir is older than STATE_LOCK_STALE_MS,
|
|
1525
|
+
// a previous holder crashed mid-write. Remove + retry once.
|
|
1526
|
+
if (!staleRecoveryUsed) {
|
|
1527
|
+
try {
|
|
1528
|
+
const st = statSync(lockDir);
|
|
1529
|
+
if (Date.now() - st.mtimeMs > STATE_LOCK_STALE_MS) {
|
|
1530
|
+
staleRecoveryUsed = true;
|
|
1531
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
} catch { /* lock vanished mid-stat; retry */ }
|
|
1535
|
+
}
|
|
1536
|
+
// Bounded busy-wait. Sync sleep via Atomics is heavy; use a tight loop
|
|
1537
|
+
// with deadline check (typical contention is microseconds in practice).
|
|
1538
|
+
const waitUntil = Date.now() + Math.min(backoff, STATE_LOCK_BACKOFF_MAX_MS, deadline - Date.now());
|
|
1539
|
+
// eslint-disable-next-line no-empty
|
|
1540
|
+
while (Date.now() < waitUntil) {}
|
|
1541
|
+
backoff = Math.min(backoff * 2, STATE_LOCK_BACKOFF_MAX_MS);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
if (!acquired) {
|
|
1546
|
+
// Couldn't acquire — fall back to caller's fn anyway. Better to risk a
|
|
1547
|
+
// racy write than to lose the re-entrancy guard entry.
|
|
1548
|
+
try { return fn(); }
|
|
1549
|
+
finally { /* no lock to release */ }
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1320
1552
|
try {
|
|
1321
|
-
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1553
|
+
return fn();
|
|
1554
|
+
} finally {
|
|
1555
|
+
try { rmSync(lockDir, { recursive: true, force: true }); } catch { /* */ }
|
|
1324
1556
|
}
|
|
1325
1557
|
}
|
|
1326
1558
|
|
|
1559
|
+
function writeStateFields(updates) {
|
|
1560
|
+
const path = join(ijfwHome(), 'state.json');
|
|
1561
|
+
withStateLockSync(() => {
|
|
1562
|
+
// Re-read INSIDE the lock so we don't merge against a stale snapshot.
|
|
1563
|
+
const state = Object.assign(readState(), updates);
|
|
1564
|
+
try {
|
|
1565
|
+
writeAtomic(path, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
|
|
1566
|
+
} catch (e) {
|
|
1567
|
+
// M11: persist failure is now visible AND surfaces which field would
|
|
1568
|
+
// not propagate. Re-entrancy guard relies on last_applied_version;
|
|
1569
|
+
// log explicitly so `ijfw doctor` / a user reading logs can spot it.
|
|
1570
|
+
console.error(`could not persist state.json (re-entrancy guard may not fire next session): ${e.message}`);
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1327
1575
|
function cmdUpdateCheck() {
|
|
1328
1576
|
const state = readState();
|
|
1329
1577
|
const current = state.installed_version || '0.0.0';
|
|
@@ -1370,6 +1618,19 @@ function cmdUpdateVerify() {
|
|
|
1370
1618
|
} else {
|
|
1371
1619
|
console.log(` provenance: NOT VERIFIED (audit signatures exited ${sig.status})`);
|
|
1372
1620
|
}
|
|
1621
|
+
// Second factor: shasum cross-verify (F-SEC-7). Dry-run: report but don't
|
|
1622
|
+
// exit non-zero -- caller is asking "what would happen if I updated?", and
|
|
1623
|
+
// the interactive flow already fails closed on mismatch.
|
|
1624
|
+
const shasumDry = verifyShasumCrossSource(r.version);
|
|
1625
|
+
if (shasumDry.mode === 'verified') {
|
|
1626
|
+
console.log(` shasum cross-verify: VERIFIED (${shasumDry.npmShasum.slice(0, 12)}...)`);
|
|
1627
|
+
} else if (shasumDry.mode === 'mismatch') {
|
|
1628
|
+
console.log(` shasum cross-verify: MISMATCH (npm=${shasumDry.npmShasum} release=${shasumDry.releaseShasum}) -- install would be REFUSED`);
|
|
1629
|
+
} else if (shasumDry.mode === 'advisory') {
|
|
1630
|
+
console.log(` shasum cross-verify: ADVISORY (${shasumDry.message})`);
|
|
1631
|
+
} else {
|
|
1632
|
+
console.log(` shasum cross-verify: ERROR (${shasumDry.message})`);
|
|
1633
|
+
}
|
|
1373
1634
|
console.log('Verification complete.');
|
|
1374
1635
|
process.exit(0);
|
|
1375
1636
|
}
|
|
@@ -1545,6 +1806,37 @@ function cmdUpdateInteractive(opts = {}) {
|
|
|
1545
1806
|
return 1;
|
|
1546
1807
|
}
|
|
1547
1808
|
}
|
|
1809
|
+
// Shasum cross-verify (F-SEC-7): independent second factor on top of
|
|
1810
|
+
// npm-side signatures. Fetches the GitLab release asset shasum and
|
|
1811
|
+
// compares it against npm's dist.shasum for the same version. Mismatch
|
|
1812
|
+
// means we refuse to install; advisory (release shasum unavailable)
|
|
1813
|
+
// requires explicit --yes to proceed.
|
|
1814
|
+
const shasum = verifyShasumCrossSource(r.version);
|
|
1815
|
+
if (shasum.mode === 'verified') {
|
|
1816
|
+
console.log(` Shasum: verified (${shasum.npmShasum.slice(0, 12)}...)`);
|
|
1817
|
+
} else if (shasum.mode === 'mismatch') {
|
|
1818
|
+
console.error(' Shasum: MISMATCH -- refusing install.');
|
|
1819
|
+
console.error(` npm : ${shasum.npmShasum}`);
|
|
1820
|
+
console.error(` release : ${shasum.releaseShasum}`);
|
|
1821
|
+
console.error(' The npm tarball does NOT match the GitLab release asset.');
|
|
1822
|
+
console.error(' This could indicate a compromised registry or release. Aborting.');
|
|
1823
|
+
return 1;
|
|
1824
|
+
} else if (shasum.mode === 'error') {
|
|
1825
|
+
console.error(` Shasum: error -- ${shasum.message}`);
|
|
1826
|
+
console.error(' Refusing install: cannot establish second-factor integrity.');
|
|
1827
|
+
return 1;
|
|
1828
|
+
} else if (shasum.mode === 'advisory') {
|
|
1829
|
+
console.error(` Shasum: ADVISORY -- ${shasum.message}`);
|
|
1830
|
+
if (!opts.yes) {
|
|
1831
|
+
console.error(' Continuing requires --yes (acknowledge missing release shasum).');
|
|
1832
|
+
return 1;
|
|
1833
|
+
}
|
|
1834
|
+
console.error(' Proceeding due to --yes; release shasum could not be verified.');
|
|
1835
|
+
} else {
|
|
1836
|
+
// Unknown mode: fail closed.
|
|
1837
|
+
console.error(` Shasum: unknown mode "${shasum.mode}" -- refusing install.`);
|
|
1838
|
+
return 1;
|
|
1839
|
+
}
|
|
1548
1840
|
// Method dispatch
|
|
1549
1841
|
const method = state.install_method || 'manual';
|
|
1550
1842
|
console.log(` install_method: ${method}`);
|
|
@@ -1637,8 +1929,17 @@ function cmdUpdateInteractive(opts = {}) {
|
|
|
1637
1929
|
console.error(`Update did not complete (exit ${installRes.status}). State not written.`);
|
|
1638
1930
|
return 1;
|
|
1639
1931
|
}
|
|
1640
|
-
// Persist both fields atomically -- single write avoids concurrent-reader inconsistency
|
|
1641
|
-
|
|
1932
|
+
// Persist both fields atomically -- single write avoids concurrent-reader inconsistency.
|
|
1933
|
+
// last_good_shasum records the shasum we just successfully cross-verified +
|
|
1934
|
+
// installed (per docs/SECURITY.md "last_good_shasum is a one-way 'what did
|
|
1935
|
+
// we actually install' record"). Only written when shasum was actually
|
|
1936
|
+
// verified (mode === 'verified'); advisory paths leave the previous value
|
|
1937
|
+
// so we don't poison the record with an unverified hash.
|
|
1938
|
+
const stateUpdate = { last_applied_version: r.version, installed_version: r.version };
|
|
1939
|
+
if (shasum && shasum.mode === 'verified' && shasum.npmShasum) {
|
|
1940
|
+
stateUpdate.last_good_shasum = shasum.npmShasum;
|
|
1941
|
+
}
|
|
1942
|
+
writeStateFields(stateUpdate);
|
|
1642
1943
|
console.log('');
|
|
1643
1944
|
console.log(`IJFW updated to v${r.version}. Run \`ijfw status\` to confirm.`);
|
|
1644
1945
|
return 0;
|
|
@@ -2017,10 +2318,25 @@ if (isMainModule) {
|
|
|
2017
2318
|
}
|
|
2018
2319
|
|
|
2019
2320
|
if (parsed.cmd === 'help') {
|
|
2020
|
-
|
|
2321
|
+
// v1.5.1 W1.D+E: orchestrator-side help is handled by the installer
|
|
2322
|
+
// (`ijfw --help` for the primary surface, `ijfw commands` for full).
|
|
2323
|
+
// Print a pointer instead of the old stale Usage block.
|
|
2324
|
+
console.log('Run `ijfw --help` for the user-facing command list, or `ijfw commands` for the full surface.');
|
|
2325
|
+
process.exit(0);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
if (parsed.cmd === 'memory-help') {
|
|
2329
|
+
printMemoryHelp();
|
|
2021
2330
|
process.exit(0);
|
|
2022
2331
|
}
|
|
2023
2332
|
|
|
2333
|
+
if (parsed.cmd === 'memory-unknown') {
|
|
2334
|
+
console.error(`Unknown memory subcommand: ${parsed.sub}`);
|
|
2335
|
+
console.error('');
|
|
2336
|
+
printMemoryHelp();
|
|
2337
|
+
process.exit(1);
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2024
2340
|
if (parsed.cmd === 'status') {
|
|
2025
2341
|
cmdStatus(process.cwd(), parsed).catch(err => { console.error(err.message); process.exit(1); });
|
|
2026
2342
|
} else if (parsed.cmd === 'demo') {
|
|
@@ -2031,6 +2347,8 @@ if (isMainModule) {
|
|
|
2031
2347
|
cmdCrossProjectAudit(parsed).catch(err => { console.error(err.message); process.exit(1); });
|
|
2032
2348
|
} else if (parsed.cmd === 'command-alias') {
|
|
2033
2349
|
cmdCommandAlias(parsed.alias);
|
|
2350
|
+
} else if (parsed.cmd === 'metrics-benchmark') {
|
|
2351
|
+
cmdMetricsBenchmark(parsed).catch(err => { console.error(err.message); process.exit(1); });
|
|
2034
2352
|
} else if (parsed.cmd === 'import') {
|
|
2035
2353
|
cmdImport(parsed).catch(err => { console.error(err.message); process.exit(1); });
|
|
2036
2354
|
} else if (parsed.cmd === 'doctor') {
|
|
@@ -2059,6 +2377,8 @@ if (isMainModule) {
|
|
|
2059
2377
|
cmdDashboard(parsed.sub);
|
|
2060
2378
|
} else if (parsed.cmd === 'design') {
|
|
2061
2379
|
cmdDesign(parsed.sub);
|
|
2380
|
+
} else if (parsed.cmd === 'ui-review') {
|
|
2381
|
+
cmdUiReview(parsed).catch(err => { console.error(err.message); process.exit(1); });
|
|
2062
2382
|
} else if (parsed.cmd === 'blackboard') {
|
|
2063
2383
|
cmdBlackboard(parsed.sub);
|
|
2064
2384
|
} else if (parsed.cmd === 'team') {
|
|
@@ -2073,11 +2393,13 @@ if (isMainModule) {
|
|
|
2073
2393
|
cmdCodex(parsed.sub);
|
|
2074
2394
|
} else if (parsed.cmd === 'memory-checkpoint') {
|
|
2075
2395
|
cmdMemoryCheckpoint(parsed.label);
|
|
2396
|
+
} else if (parsed.cmd === 'memory-reindex') {
|
|
2397
|
+
cmdMemoryReindex(parsed).catch(err => { console.error(err.message); process.exit(1); });
|
|
2076
2398
|
} else if (parsed.cmd === 'recover') {
|
|
2077
2399
|
cmdRecover(parsed.sub);
|
|
2078
2400
|
} else {
|
|
2079
|
-
|
|
2080
|
-
|
|
2401
|
+
// v1.5.1 W1.D+E: clean unknown-command message; no stale usage dump.
|
|
2402
|
+
printUnknownCommand(parsed.raw);
|
|
2081
2403
|
process.exit(1);
|
|
2082
2404
|
}
|
|
2083
2405
|
}
|
|
@@ -2158,6 +2480,69 @@ function openDesignUrl(url) {
|
|
|
2158
2480
|
return res.status ?? 0;
|
|
2159
2481
|
}
|
|
2160
2482
|
|
|
2483
|
+
// v1.5.0 wire-W1.D — `ijfw ui-review` production CLI. Wires the 7-pillar
|
|
2484
|
+
// audit runner into a user-facing command. Args:
|
|
2485
|
+
// --spec <UI-SPEC.md path> required
|
|
2486
|
+
// --scope <comma-sep dirs> required (e.g. "src,components")
|
|
2487
|
+
// --no-write skip writing UI-REVIEW.md (preview mode)
|
|
2488
|
+
// --gc-sketches run sketches-gc as the finalizer
|
|
2489
|
+
// --peer-inputs <json-path> optional axe / lighthouse / playwright
|
|
2490
|
+
// pre-computed outputs (JSON file)
|
|
2491
|
+
// --json machine-readable output (skip narrative)
|
|
2492
|
+
async function cmdUiReview(parsed) {
|
|
2493
|
+
if (!parsed.spec) {
|
|
2494
|
+
console.error('Usage: ijfw ui-review --spec <UI-SPEC.md> --scope <dirs> [--no-write] [--gc-sketches] [--peer-inputs <path>]');
|
|
2495
|
+
process.exit(1);
|
|
2496
|
+
}
|
|
2497
|
+
if (!parsed.scope) {
|
|
2498
|
+
console.error('ui-review: --scope is required (comma-separated dirs, e.g. "src,components")');
|
|
2499
|
+
process.exit(1);
|
|
2500
|
+
}
|
|
2501
|
+
const specPath = isAbsolute(parsed.spec) ? parsed.spec : resolve(process.cwd(), parsed.spec);
|
|
2502
|
+
if (!existsSync(specPath)) {
|
|
2503
|
+
console.error(`ui-review: UI-SPEC not found at ${specPath}`);
|
|
2504
|
+
process.exit(1);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
let peerInputs = {};
|
|
2508
|
+
if (parsed.peerInputs) {
|
|
2509
|
+
const ppath = isAbsolute(parsed.peerInputs) ? parsed.peerInputs : resolve(process.cwd(), parsed.peerInputs);
|
|
2510
|
+
try { peerInputs = JSON.parse(readFileSync(ppath, 'utf8')); }
|
|
2511
|
+
catch (err) {
|
|
2512
|
+
console.error(`ui-review: failed to read --peer-inputs JSON: ${err.message}`);
|
|
2513
|
+
process.exit(1);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
const { runUiReview } = await import('./lib/ui-review-runner.js');
|
|
2518
|
+
const result = await runUiReview({
|
|
2519
|
+
uiSpecPath: specPath,
|
|
2520
|
+
sourceScope: parsed.scope,
|
|
2521
|
+
projectRoot: process.cwd(),
|
|
2522
|
+
peerInputs,
|
|
2523
|
+
write: parsed.write !== false,
|
|
2524
|
+
gcSketches: !!parsed.gcSketches,
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2527
|
+
if (parsed.json) {
|
|
2528
|
+
console.log(JSON.stringify({
|
|
2529
|
+
topVerdict: result.topVerdict,
|
|
2530
|
+
pillarVerdicts: result.pillarVerdicts,
|
|
2531
|
+
reviewPath: result.reviewPath,
|
|
2532
|
+
parallel: result.parallel,
|
|
2533
|
+
}, null, 2));
|
|
2534
|
+
} else {
|
|
2535
|
+
console.log(`UI review: top-level ${result.topVerdict}`);
|
|
2536
|
+
for (const [pillar, verdict] of Object.entries(result.pillarVerdicts)) {
|
|
2537
|
+
console.log(` - ${pillar.padEnd(12)} ${verdict}`);
|
|
2538
|
+
}
|
|
2539
|
+
if (result.reviewPath) console.log(`Review written to: ${result.reviewPath}`);
|
|
2540
|
+
console.log(`Parallel grader run: wall=${result.parallel.wallMs}ms parallelism=${result.parallel.parallelism}`);
|
|
2541
|
+
}
|
|
2542
|
+
// Exit code: PASS=0, FLAG=0 (advisory), BLOCK=2 (ship-blocker)
|
|
2543
|
+
process.exit(result.topVerdict === 'BLOCK' ? 2 : 0);
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2161
2546
|
function cmdDesign(sub) {
|
|
2162
2547
|
const contentDir = join(homedir(), '.ijfw', 'design-companion', 'content');
|
|
2163
2548
|
mkdirSync(contentDir, { recursive: true });
|
|
@@ -2369,8 +2754,11 @@ function cmdTeam(sub) {
|
|
|
2369
2754
|
if (sub === 'init' || sub === 'create') {
|
|
2370
2755
|
const archetype = optionValue(args, ['--archetype', '--type', '-t']);
|
|
2371
2756
|
const teamName = optionValue(args, ['--name']);
|
|
2757
|
+
// F-FUN-1: --brief lets the Discovery hand-off carry domain signal
|
|
2758
|
+
// when filesystem detection would otherwise collapse to mixed/unknown.
|
|
2759
|
+
const brief = optionValue(args, ['--brief', '-b']) || '';
|
|
2372
2760
|
const force = args.includes('--force');
|
|
2373
|
-
const result = createTeamAssembly(process.cwd(), { archetype, teamName, force });
|
|
2761
|
+
const result = createTeamAssembly(process.cwd(), { archetype, teamName, brief, force });
|
|
2374
2762
|
if (!result.ok) {
|
|
2375
2763
|
if (result.error === 'exists') {
|
|
2376
2764
|
console.error('Team assembly already exists. Re-run with --force to replace .ijfw/team/charter.json and workflow.json.');
|
|
@@ -2405,7 +2793,101 @@ function cmdTeam(sub) {
|
|
|
2405
2793
|
process.exit(0);
|
|
2406
2794
|
}
|
|
2407
2795
|
|
|
2408
|
-
|
|
2796
|
+
// F-FUN-4 (audit-MED-teams-#7): ijfw team list -- enumerate roles.
|
|
2797
|
+
if (sub === 'list') {
|
|
2798
|
+
const result = listTeamRoles(process.cwd());
|
|
2799
|
+
if (!result.ok) {
|
|
2800
|
+
console.error(`team list failed: ${result.error}`);
|
|
2801
|
+
process.exit(1);
|
|
2802
|
+
}
|
|
2803
|
+
console.log(`Team: ${result.team_name || '(unnamed)'}`);
|
|
2804
|
+
if (result.project_archetypes.length) console.log(`Archetypes: ${result.project_archetypes.join(', ')}`);
|
|
2805
|
+
console.log(`Roles (${result.roles.length}):`);
|
|
2806
|
+
for (const role of result.roles) {
|
|
2807
|
+
console.log(` - ${role.name} [${role.role_type}] model=${role.model} effort=${role.effort}`);
|
|
2808
|
+
}
|
|
2809
|
+
process.exit(0);
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
// F-FUN-4: ijfw team add <role-name> --charter <path>
|
|
2813
|
+
if (sub === 'add') {
|
|
2814
|
+
const name = args[0] && !args[0].startsWith('--') ? args[0] : null;
|
|
2815
|
+
const charterPath = optionValue(args, ['--charter', '-c']);
|
|
2816
|
+
if (!charterPath) {
|
|
2817
|
+
console.error('Usage: ijfw team add <role-name> --charter <path-to-role.json>');
|
|
2818
|
+
process.exit(1);
|
|
2819
|
+
}
|
|
2820
|
+
const result = addTeamRole(process.cwd(), { charterPath });
|
|
2821
|
+
if (!result.ok) {
|
|
2822
|
+
console.error(`team add failed: ${result.error}`);
|
|
2823
|
+
if (result.errors) result.errors.forEach((e) => console.error(` - ${e}`));
|
|
2824
|
+
process.exit(1);
|
|
2825
|
+
}
|
|
2826
|
+
console.log(`Added role: ${result.role.name}`);
|
|
2827
|
+
if (name && name !== result.role.name) {
|
|
2828
|
+
console.log(` note: charter declared name "${result.role.name}", ignoring CLI argument "${name}"`);
|
|
2829
|
+
}
|
|
2830
|
+
if (result.codex?.ok) console.log(`Codex agents resynced: ${result.codex.count} (${result.codex.skipped ?? 0} unchanged)`);
|
|
2831
|
+
process.exit(0);
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// F-FUN-4: ijfw team remove <role-name>
|
|
2835
|
+
if (sub === 'remove' || sub === 'rm') {
|
|
2836
|
+
const name = args[0] && !args[0].startsWith('--') ? args[0] : null;
|
|
2837
|
+
if (!name) {
|
|
2838
|
+
console.error('Usage: ijfw team remove <role-name>');
|
|
2839
|
+
process.exit(1);
|
|
2840
|
+
}
|
|
2841
|
+
const result = removeTeamRole(process.cwd(), { name });
|
|
2842
|
+
if (!result.ok) {
|
|
2843
|
+
console.error(`team remove failed: ${result.error}`);
|
|
2844
|
+
if (result.errors) result.errors.forEach((e) => console.error(` - ${e}`));
|
|
2845
|
+
process.exit(1);
|
|
2846
|
+
}
|
|
2847
|
+
console.log(`Removed role: ${result.removed}`);
|
|
2848
|
+
if (result.codex?.ok) console.log(`Codex agents resynced: ${result.codex.count} (${result.codex.skipped ?? 0} unchanged)`);
|
|
2849
|
+
process.exit(0);
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// F-FUN-4: ijfw team swap <old-role-name> --charter <path>
|
|
2853
|
+
if (sub === 'swap') {
|
|
2854
|
+
const oldName = args[0] && !args[0].startsWith('--') ? args[0] : null;
|
|
2855
|
+
const charterPath = optionValue(args, ['--charter', '-c']);
|
|
2856
|
+
if (!oldName || !charterPath) {
|
|
2857
|
+
console.error('Usage: ijfw team swap <old-role-name> --charter <path-to-role.json>');
|
|
2858
|
+
process.exit(1);
|
|
2859
|
+
}
|
|
2860
|
+
const result = swapTeamRole(process.cwd(), { oldName, charterPath });
|
|
2861
|
+
if (!result.ok) {
|
|
2862
|
+
console.error(`team swap failed: ${result.error}`);
|
|
2863
|
+
if (result.errors) result.errors.forEach((e) => console.error(` - ${e}`));
|
|
2864
|
+
process.exit(1);
|
|
2865
|
+
}
|
|
2866
|
+
console.log(`Swapped ${result.swapped.old} -> ${result.swapped.new}`);
|
|
2867
|
+
if (result.codex?.ok) console.log(`Codex agents resynced: ${result.codex.count} (${result.codex.skipped ?? 0} unchanged)`);
|
|
2868
|
+
process.exit(0);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// F-FUN-5 (audit-MED-teams-#13): ijfw team check -- standalone validator.
|
|
2872
|
+
if (sub === 'check' || sub === 'validate') {
|
|
2873
|
+
const report = checkTeamAssembly(process.cwd());
|
|
2874
|
+
if (report.ok) {
|
|
2875
|
+
console.log(`Team assembly OK: ${report.role_count} role(s), ${report.artifact_count} artifact(s)`);
|
|
2876
|
+
process.exit(0);
|
|
2877
|
+
}
|
|
2878
|
+
console.error('Team assembly has issues:');
|
|
2879
|
+
if (!report.has_charter || !report.charter.ok) {
|
|
2880
|
+
console.error(' charter.json:');
|
|
2881
|
+
for (const err of report.charter.errors) console.error(` - ${err}`);
|
|
2882
|
+
}
|
|
2883
|
+
if (!report.has_workflow || !report.workflow.ok) {
|
|
2884
|
+
console.error(' workflow.json:');
|
|
2885
|
+
for (const err of report.workflow.errors) console.error(` - ${err}`);
|
|
2886
|
+
}
|
|
2887
|
+
process.exit(1);
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
console.log('Usage: ijfw team init [--archetype <type>] [--name <team-name>] [--force] | status | list | add <role> --charter <path> | remove <role> | swap <old> --charter <path> | check');
|
|
2409
2891
|
process.exit(1);
|
|
2410
2892
|
}
|
|
2411
2893
|
|
|
@@ -2483,11 +2965,23 @@ function codexDoctor(projectRoot) {
|
|
|
2483
2965
|
fix: 'restore codex/.codex/hooks.json and hook scripts',
|
|
2484
2966
|
});
|
|
2485
2967
|
|
|
2968
|
+
// C11 — the message MUST track the same condition `ok` does. Previously the
|
|
2969
|
+
// message said "ijfw-memory configured" whenever config.toml merely existed,
|
|
2970
|
+
// so a config.toml present-but-missing-the-ijfw-memory-block printed the
|
|
2971
|
+
// [ !! ] failure glyph (ok=false) next to success text. Branch all three
|
|
2972
|
+
// states: file absent, file present-but-unconfigured, file configured.
|
|
2973
|
+
const _codexConfigExists = existsSync(configPath);
|
|
2974
|
+
const _codexMemoryConfigured =
|
|
2975
|
+
_codexConfigExists && readFileSync(configPath, 'utf8').includes('ijfw-memory');
|
|
2486
2976
|
checks.push({
|
|
2487
2977
|
name: 'MCP config',
|
|
2488
|
-
ok:
|
|
2978
|
+
ok: _codexMemoryConfigured,
|
|
2489
2979
|
required: true,
|
|
2490
|
-
message:
|
|
2980
|
+
message: _codexMemoryConfigured
|
|
2981
|
+
? 'ijfw-memory configured'
|
|
2982
|
+
: _codexConfigExists
|
|
2983
|
+
? 'config.toml present but ijfw-memory not configured'
|
|
2984
|
+
: 'missing config.toml',
|
|
2491
2985
|
fix: 'run ijfw install or restore codex/.codex/config.toml',
|
|
2492
2986
|
});
|
|
2493
2987
|
|
|
@@ -2730,7 +3224,45 @@ function cmdSwarm(sub) {
|
|
|
2730
3224
|
process.exit(0);
|
|
2731
3225
|
}
|
|
2732
3226
|
|
|
2733
|
-
|
|
3227
|
+
// F-REL-1 (H5.3): orphan eviction. Walks active claims and releases any
|
|
3228
|
+
// whose freshness anchor (claimed_at or heartbeat_at) is older than the
|
|
3229
|
+
// TTL. Default TTL is 30 minutes; override with --ttl-min N.
|
|
3230
|
+
if (sub === 'evict-orphans' || sub === 'evict') {
|
|
3231
|
+
const minRaw = optionValue(args, ['--ttl-min', '--ttl', '-t']);
|
|
3232
|
+
const min = Number.parseFloat(minRaw);
|
|
3233
|
+
const ttlMs = Number.isFinite(min) && min > 0 ? Math.floor(min * 60 * 1000) : DEFAULT_CLAIM_TTL_MS;
|
|
3234
|
+
const result = evictOrphanedClaims(process.cwd(), { ttlMs });
|
|
3235
|
+
if (!result.ok) {
|
|
3236
|
+
console.log(`Swarm evict halted: ${result.error}`);
|
|
3237
|
+
process.exit(1);
|
|
3238
|
+
}
|
|
3239
|
+
console.log(`Evicted ${result.count} orphan claim(s) (TTL ${Math.round(ttlMs / 60000)}min)`);
|
|
3240
|
+
for (const item of result.evicted) {
|
|
3241
|
+
console.log(` ${item.id} (${item.agent} -> ${item.artifact_id}, age ${Math.round(item.age_ms / 1000)}s)`);
|
|
3242
|
+
}
|
|
3243
|
+
process.exit(0);
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// F-REL-1 (H5.3): manual heartbeat ping. Subagents normally invoke this
|
|
3247
|
+
// via the programmatic surface (updateClaimHeartbeat); the CLI form is
|
|
3248
|
+
// for forensic dogfooding from tests + the dashboard preview.
|
|
3249
|
+
if (sub === 'heartbeat') {
|
|
3250
|
+
const taskOrClaim = args[0];
|
|
3251
|
+
const owner = optionValue(args.slice(1), ['--owner', '-o']);
|
|
3252
|
+
const claimId = optionValue(args.slice(1), ['--claim', '--id']);
|
|
3253
|
+
const input = claimId
|
|
3254
|
+
? { claim_id: claimId }
|
|
3255
|
+
: { artifact_id: taskOrClaim, agent: owner };
|
|
3256
|
+
const result = updateClaimHeartbeat(process.cwd(), input);
|
|
3257
|
+
if (!result.ok) {
|
|
3258
|
+
console.log(`Heartbeat halted: ${result.error}`);
|
|
3259
|
+
process.exit(1);
|
|
3260
|
+
}
|
|
3261
|
+
console.log(`Heartbeat ${result.claim.id} at ${result.claim.heartbeat_at}`);
|
|
3262
|
+
process.exit(0);
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
console.log('Usage: ijfw swarm plan | prepare [--append] [--reviews] | tasks | prompt <task-id> [--codex] | start <task-id> [--owner <agent>] | complete <task-id> [--message <text>] | block <task-id> --message <why> | ready <task-id> | status | evict-orphans [--ttl-min N] | heartbeat <artifact-id> --owner <agent>');
|
|
2734
3266
|
process.exit(1);
|
|
2735
3267
|
}
|
|
2736
3268
|
|
|
@@ -2819,6 +3351,69 @@ function cmdMemoryCheckpoint(label) {
|
|
|
2819
3351
|
process.exit(0);
|
|
2820
3352
|
}
|
|
2821
3353
|
|
|
3354
|
+
// v1.5.1 R5-1.2 -- `ijfw memory reindex [--m2]`. Closes Trident r5 finding
|
|
3355
|
+
// 1.2: memory written during v1.5.0 (before Round-4 Fix-1 wired M1/M2 into
|
|
3356
|
+
// the production write path) has empty memory_links / memory_tags /
|
|
3357
|
+
// memory_meta. Migration 009 already backfills M1 once on upgrade; this verb
|
|
3358
|
+
// is the manual re-run path AND the only way to opt into the M2 (A-Mem
|
|
3359
|
+
// auto-link) backfill, which is budget-gated because it makes one LLM call
|
|
3360
|
+
// per row.
|
|
3361
|
+
async function cmdMemoryReindex(parsed) {
|
|
3362
|
+
const projectRoot = process.cwd();
|
|
3363
|
+
// Lazy import: better-sqlite3 is heavy; only pay for it on this verb.
|
|
3364
|
+
const { openDb, closeDb, dbPathFor } = await import('./memory/fts5.js');
|
|
3365
|
+
const { backfillObsidianIndex } = await import('./memory/obsidian-parser.js');
|
|
3366
|
+
const { backfillAutoLink } = await import('./memory/auto-linker.js');
|
|
3367
|
+
|
|
3368
|
+
let db;
|
|
3369
|
+
try {
|
|
3370
|
+
db = await openDb(projectRoot);
|
|
3371
|
+
} catch (e) {
|
|
3372
|
+
console.error(`Memory db unavailable: ${e.message}`);
|
|
3373
|
+
process.exit(1);
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
try {
|
|
3377
|
+
console.log(`Reindexing memory at ${dbPathFor(projectRoot)}`);
|
|
3378
|
+
// M1 -- always. Free + idempotent obsidian indexing.
|
|
3379
|
+
const m1 = backfillObsidianIndex(db);
|
|
3380
|
+
console.log(
|
|
3381
|
+
`M1 obsidian-index backfill: ${m1.rows} entries re-indexed ` +
|
|
3382
|
+
`(${m1.links} links, ${m1.tags} tags, ${m1.meta} meta` +
|
|
3383
|
+
`${m1.errors ? `, ${m1.errors} errors` : ''}).`,
|
|
3384
|
+
);
|
|
3385
|
+
|
|
3386
|
+
// M2 -- opt-in via --m2. Budget-gated; backfillAutoLink internally
|
|
3387
|
+
// forces past the IJFW_AUTOLINK_BACKFILL opt-in (the --m2 flag IS the
|
|
3388
|
+
// explicit opt-in) but still honours IJFW_AUTOLINK_OFF, the budget cap,
|
|
3389
|
+
// and the API-key requirement.
|
|
3390
|
+
if (parsed.m2) {
|
|
3391
|
+
const m2 = await backfillAutoLink(db, { force: true });
|
|
3392
|
+
if (m2.skipped) {
|
|
3393
|
+
console.log(
|
|
3394
|
+
`M2 auto-link backfill skipped (${m2.reason}). ` +
|
|
3395
|
+
`M2 backfill needs a positive IJFW_AUTOLINK_BUDGET_USD cap and an ` +
|
|
3396
|
+
`API key (IJFW_AUTOLINK_API_KEY or ANTHROPIC_API_KEY).`,
|
|
3397
|
+
);
|
|
3398
|
+
} else {
|
|
3399
|
+
console.log(
|
|
3400
|
+
`M2 auto-link backfill: ${m2.linked}/${m2.rows} entries linked ` +
|
|
3401
|
+
`(${m2.links_added} links, ${m2.neighbor_tags_added} neighbor tags)` +
|
|
3402
|
+
`${m2.stopped_early ? ' -- stopped early (budget / kill switch)' : ''}.`,
|
|
3403
|
+
);
|
|
3404
|
+
}
|
|
3405
|
+
} else {
|
|
3406
|
+
console.log(
|
|
3407
|
+
'M2 auto-link backfill not run. Re-run with --m2 (budget-gated) to ' +
|
|
3408
|
+
'auto-link old entries via the A-Mem LLM pass.',
|
|
3409
|
+
);
|
|
3410
|
+
}
|
|
3411
|
+
} finally {
|
|
3412
|
+
closeDb(db);
|
|
3413
|
+
}
|
|
3414
|
+
process.exit(0);
|
|
3415
|
+
}
|
|
3416
|
+
|
|
2822
3417
|
function cmdRecover(sub) {
|
|
2823
3418
|
if (sub === 'latest') {
|
|
2824
3419
|
const latest = latestCheckpoint(process.cwd());
|