@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
package/src/team/generator.js
CHANGED
|
@@ -10,15 +10,274 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
import { writeAtomic } from '../lib/atomic-io.js';
|
|
11
11
|
import { syncCodexAgents } from '../codex-agents.js';
|
|
12
12
|
import { detect } from '../project-type-detector.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
DOMAIN_SPECIALIST_AGENT_IDS as CANONICAL_DOMAIN_SPECIALIST_AGENT_IDS,
|
|
15
|
+
SOFTWARE_CORE_AGENT_IDS as CANONICAL_SOFTWARE_CORE_AGENT_IDS,
|
|
16
|
+
assertValidTeamBundle,
|
|
17
|
+
validateTeamCharter,
|
|
18
|
+
validateWorkflowManifest,
|
|
19
|
+
} from './schemas.js';
|
|
14
20
|
|
|
15
21
|
const FIXTURE_DIR = resolve(fileURLToPath(new URL('../../fixtures/team/', import.meta.url)));
|
|
22
|
+
// W1.5.B (ADR W1.5 Option C "merge"): domain-templates is the canonical
|
|
23
|
+
// agent-id spec; fixtures remain canonical for the executable team bundle.
|
|
24
|
+
// `loadTeamTemplate` cross-validates the two sources at load time so any
|
|
25
|
+
// future drift surfaces as a load-time signal rather than a silent
|
|
26
|
+
// runtime mismatch. See `.planning/1.5.1/decisions/W1.5-canonical-source.md`.
|
|
27
|
+
const DOMAIN_TEMPLATES_DIR = resolve(fileURLToPath(new URL('./domain-templates/', import.meta.url)));
|
|
16
28
|
const SUPPORTED_ARCHETYPES = new Set(['software', 'design', 'content', 'book', 'research', 'business', 'mixed']);
|
|
17
29
|
|
|
30
|
+
// W1.5.B: one-time warning suppression per archetype so drift between
|
|
31
|
+
// fixtures and domain-templates emits a single, clearly-attributed
|
|
32
|
+
// warning rather than spamming stderr on every loadTeamTemplate call
|
|
33
|
+
// (tests + the CLI may both call into the loader inside the same process).
|
|
34
|
+
const _crossValidationWarned = new Set();
|
|
35
|
+
|
|
36
|
+
// T24 / G7-core: the four universal software-core agents. Any software-
|
|
37
|
+
// domain roster MUST include all four. The ids resolve to static markdown
|
|
38
|
+
// files under `claude/agents/<id>.md`; the generator does not synthesise
|
|
39
|
+
// these — it references them by id and trusts the installer to deploy the
|
|
40
|
+
// markdown files into platform-native agent directories.
|
|
41
|
+
//
|
|
42
|
+
// Single source of truth lives in `./schemas.js` (so downstream validators
|
|
43
|
+
// can reference the canonical list without importing the generator). This
|
|
44
|
+
// re-export keeps the historic generator.js surface intact for callers
|
|
45
|
+
// already wired to `SOFTWARE_CORE_AGENT_IDS`.
|
|
46
|
+
//
|
|
47
|
+
// Static set (order is deterministic for snapshot stability):
|
|
48
|
+
// - ijfw-doc-verifier — factual-claim verification post-doc-gen
|
|
49
|
+
// - ijfw-integration-checker — cross-subagent E2E flow verification
|
|
50
|
+
// - ijfw-nyquist-auditor — coverage-gap closure + skeleton-test proposals
|
|
51
|
+
// - ijfw-code-fixer — atomic per-finding code fixes (G4 fixer)
|
|
52
|
+
export const SOFTWARE_CORE_AGENT_IDS = CANONICAL_SOFTWARE_CORE_AGENT_IDS;
|
|
53
|
+
|
|
54
|
+
// T25 / G7-gen: canonical per-domain specialist agent ids. Re-exported from
|
|
55
|
+
// schemas.js so callers wired to `generator.js` get a stable surface. See
|
|
56
|
+
// schemas.js for the per-archetype contract and the rationale for which
|
|
57
|
+
// archetypes are populated today.
|
|
58
|
+
export const DOMAIN_SPECIALIST_AGENT_IDS = CANONICAL_DOMAIN_SPECIALIST_AGENT_IDS;
|
|
59
|
+
|
|
60
|
+
// T24: archetypes that always include the software-core agent set.
|
|
61
|
+
// Currently only `software`; future domains (`mixed` with software files)
|
|
62
|
+
// may opt in via T25's domain-aware generator.
|
|
63
|
+
const SOFTWARE_CORE_ARCHETYPES = new Set(['software']);
|
|
64
|
+
|
|
65
|
+
// F-FUN-1: alias map -- detector returns language-flavoured labels and
|
|
66
|
+
// project-type-detector emits 'unknown' / unmapped domains. Canonicalize
|
|
67
|
+
// BEFORE the SUPPORTED_ARCHETYPES gate so detector outputs don't collapse
|
|
68
|
+
// to 'mixed' just because the literal string isn't in the supported set.
|
|
69
|
+
const ARCHETYPE_ALIASES = new Map([
|
|
70
|
+
// language-specific aliases that some detector paths surface
|
|
71
|
+
['typescript', 'software'],
|
|
72
|
+
['javascript', 'software'],
|
|
73
|
+
['python', 'software'],
|
|
74
|
+
['rust', 'software'],
|
|
75
|
+
['go', 'software'],
|
|
76
|
+
['java', 'software'],
|
|
77
|
+
['ruby', 'software'],
|
|
78
|
+
['php', 'software'],
|
|
79
|
+
['cpp', 'software'],
|
|
80
|
+
['c++', 'software'],
|
|
81
|
+
['csharp', 'software'],
|
|
82
|
+
['code', 'software'],
|
|
83
|
+
['app', 'software'],
|
|
84
|
+
['api', 'software'],
|
|
85
|
+
// domain synonyms surfaced by briefs and detector secondaries
|
|
86
|
+
['marketing', 'content'],
|
|
87
|
+
['campaign', 'content'],
|
|
88
|
+
['launch', 'content'],
|
|
89
|
+
['blog', 'content'],
|
|
90
|
+
['copy', 'content'],
|
|
91
|
+
['ui', 'design'],
|
|
92
|
+
['ux', 'design'],
|
|
93
|
+
['novel', 'book'],
|
|
94
|
+
['story', 'book'],
|
|
95
|
+
['manuscript', 'book'],
|
|
96
|
+
['paper', 'research'],
|
|
97
|
+
['study', 'research'],
|
|
98
|
+
['thesis', 'research'],
|
|
99
|
+
['strategy', 'business'],
|
|
100
|
+
['ops', 'business'],
|
|
101
|
+
['operations', 'business'],
|
|
102
|
+
['education', 'mixed'],
|
|
103
|
+
['unknown', 'mixed'],
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
// F-FUN-1: brief keyword maps. Each phrase scores +1 toward the listed
|
|
107
|
+
// archetype on whole-word hit. Word boundaries matter -- "research" matches
|
|
108
|
+
// in "research project" but not "researching" -- so we tokenize the brief
|
|
109
|
+
// before scoring. Mirrors the canonical domain list in team-templates.md.
|
|
110
|
+
const BRIEF_KEYWORDS = {
|
|
111
|
+
software: [
|
|
112
|
+
'software', 'app', 'application', 'api', 'code', 'codebase', 'service',
|
|
113
|
+
'webapp', 'backend', 'frontend', 'mobile', 'sdk', 'library', 'cli',
|
|
114
|
+
'feature', 'endpoint', 'module', 'refactor', 'bugfix', 'plugin',
|
|
115
|
+
'platform', 'integration', 'pipeline',
|
|
116
|
+
],
|
|
117
|
+
book: [
|
|
118
|
+
'book', 'novel', 'novella', 'story', 'chapter', 'chapters', 'manuscript',
|
|
119
|
+
'memoir', 'fiction', 'nonfiction', 'prose', 'narrative', 'screenplay',
|
|
120
|
+
'cookbook', 'anthology',
|
|
121
|
+
],
|
|
122
|
+
content: [
|
|
123
|
+
'campaign', 'launch', 'marketing', 'content', 'blog', 'article',
|
|
124
|
+
'newsletter', 'copy', 'copywriting', 'landing', 'social', 'seo',
|
|
125
|
+
'email', 'post', 'posts', 'announcement', 'press',
|
|
126
|
+
],
|
|
127
|
+
design: [
|
|
128
|
+
'design', 'ui', 'ux', 'wireframe', 'mockup', 'figma', 'prototype',
|
|
129
|
+
'visual', 'brand', 'logo', 'illustration', 'typography', 'palette',
|
|
130
|
+
'design-system',
|
|
131
|
+
],
|
|
132
|
+
research: [
|
|
133
|
+
'research', 'paper', 'study', 'thesis', 'methodology', 'experiment',
|
|
134
|
+
'literature', 'corpus', 'survey', 'analysis', 'hypothesis', 'findings',
|
|
135
|
+
'whitepaper',
|
|
136
|
+
],
|
|
137
|
+
business: [
|
|
138
|
+
'business', 'strategy', 'operations', 'ops', 'financial', 'finance',
|
|
139
|
+
'budget', 'forecast', 'plan', 'pitch', 'investor', 'gtm', 'b2b', 'b2c',
|
|
140
|
+
'revenue', 'roadmap', 'okrs',
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// F-FUN-1: brief signal threshold. A single keyword hit isn't enough to
|
|
145
|
+
// override filesystem signals (a software repo whose README says "we plan to
|
|
146
|
+
// research X" should stay software). Two distinct keyword hits OR a strong
|
|
147
|
+
// signal phrase ("write a book", "marketing campaign", "research paper")
|
|
148
|
+
// flips the result. Phrases score double because they're explicit.
|
|
149
|
+
const BRIEF_PHRASES = {
|
|
150
|
+
software: [/\b(build|ship|develop|implement)\s+(?:a|an|the)?\s*(app|application|api|service|feature|module|library)\b/i, /\bsource\s+code\b/i],
|
|
151
|
+
book: [/\bwrite\s+(?:a|an|the)?\s*(book|novel|memoir|story|chapter)\b/i, /\bbook\s+about\b/i],
|
|
152
|
+
content: [/\b(marketing|launch|content|seo|social\s+media)\s+(campaign|strategy|plan|push)\b/i, /\bblog\s+post\b/i],
|
|
153
|
+
design: [/\b(design|brand|ui|ux)\s+(system|kit|guide|language|review)\b/i, /\bwireframe(?:s|d)?\s+(?:the|for|of)\b/i],
|
|
154
|
+
research: [/\bresearch\s+(paper|project|study|report|brief)\b/i, /\bliterature\s+review\b/i],
|
|
155
|
+
business: [/\b(business|strategy|operations)\s+(plan|roadmap|memo)\b/i, /\binvestor\s+(deck|pitch|memo)\b/i],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const BRIEF_SCORE_FLIP_THRESHOLD = 2;
|
|
159
|
+
|
|
18
160
|
export function detectTeamArchetype(projectRoot = process.cwd(), options = {}) {
|
|
19
161
|
if (options.archetype) return normalizeArchetype(options.archetype);
|
|
162
|
+
|
|
163
|
+
// F-FUN-1: brief-first archetype routing. When the caller hands us a
|
|
164
|
+
// non-empty project brief, score it for explicit domain signals before
|
|
165
|
+
// falling back to filesystem-only detection. An explicit brief domain
|
|
166
|
+
// (e.g. "I'm writing a book about ...") MUST outweigh the filesystem
|
|
167
|
+
// signal -- otherwise book/research/campaign projects collapse to 'mixed'
|
|
168
|
+
// when run from a tmp directory or a freshly cloned repo with no files.
|
|
169
|
+
const brief = typeof options.brief === 'string' ? options.brief : '';
|
|
170
|
+
const briefScores = scoreBrief(brief);
|
|
171
|
+
const briefWinner = topBriefDomain(briefScores);
|
|
172
|
+
|
|
20
173
|
const detected = detect(projectRoot, { maxFiles: options.maxFiles || 4000, c9Available: false });
|
|
21
|
-
|
|
174
|
+
const detectedArchetype = normalizeArchetype(detected.primary_type || detected.type);
|
|
175
|
+
|
|
176
|
+
if (briefWinner) return briefWinner;
|
|
177
|
+
return detectedArchetype;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// F-FUN-1: brief scorer. Returns a map of {archetype: score}. Word-boundary
|
|
181
|
+
// match for single tokens, regex match for phrase signals (which count
|
|
182
|
+
// double). The caller picks the winner.
|
|
183
|
+
export function scoreBrief(brief) {
|
|
184
|
+
const scores = { software: 0, book: 0, content: 0, design: 0, research: 0, business: 0 };
|
|
185
|
+
if (!brief || typeof brief !== 'string') return scores;
|
|
186
|
+
const text = brief.toLowerCase();
|
|
187
|
+
|
|
188
|
+
for (const [domain, tokens] of Object.entries(BRIEF_KEYWORDS)) {
|
|
189
|
+
for (const token of tokens) {
|
|
190
|
+
const re = new RegExp(`\\b${escapeRegExp(token)}\\b`, 'i');
|
|
191
|
+
if (re.test(text)) scores[domain] += 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const [domain, patterns] of Object.entries(BRIEF_PHRASES)) {
|
|
196
|
+
for (const re of patterns) {
|
|
197
|
+
if (re.test(text)) scores[domain] += 2;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return scores;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function topBriefDomain(scores) {
|
|
205
|
+
let best = null;
|
|
206
|
+
let bestScore = 0;
|
|
207
|
+
for (const [domain, score] of Object.entries(scores)) {
|
|
208
|
+
if (score > bestScore) {
|
|
209
|
+
best = domain;
|
|
210
|
+
bestScore = score;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Require at least the flip threshold AND a clear margin over runner-up.
|
|
214
|
+
// Without margin, "research the market for an app" ties software vs
|
|
215
|
+
// research and we should fall through to filesystem detection instead.
|
|
216
|
+
if (bestScore < BRIEF_SCORE_FLIP_THRESHOLD) return null;
|
|
217
|
+
const second = Object.entries(scores)
|
|
218
|
+
.filter(([d]) => d !== best)
|
|
219
|
+
.reduce((m, [, s]) => Math.max(m, s), 0);
|
|
220
|
+
if (second >= bestScore) return null;
|
|
221
|
+
return best;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function escapeRegExp(s) {
|
|
225
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// T24 / G7-core: resolve the static software-core agent set for an
|
|
229
|
+
// archetype. Returns `[]` for non-software archetypes. The returned ids
|
|
230
|
+
// each resolve to `claude/agents/<id>.md` in the IJFW repo — callers can
|
|
231
|
+
// look the files up via the installer's deploy step (the markdown is the
|
|
232
|
+
// agent spec; the generator doesn't render its content, it references
|
|
233
|
+
// it). Deterministic order.
|
|
234
|
+
export function resolveSoftwareCoreAgentIds(archetype) {
|
|
235
|
+
const normalized = normalizeArchetype(archetype);
|
|
236
|
+
if (!SOFTWARE_CORE_ARCHETYPES.has(normalized)) return [];
|
|
237
|
+
// Return a fresh array so callers cannot mutate the frozen source set
|
|
238
|
+
// through the public surface.
|
|
239
|
+
return [...SOFTWARE_CORE_AGENT_IDS];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// T25 / G7-gen: resolve the per-domain specialist agent set. Returns the
|
|
243
|
+
// archetype's specialist ids per `DOMAIN_SPECIALIST_AGENT_IDS`, or `[]` if
|
|
244
|
+
// the archetype has no domain specialists yet (e.g. `research`, `business`,
|
|
245
|
+
// `mixed`). For `software` this returns `[]` — the software roster is
|
|
246
|
+
// covered by `resolveSoftwareCoreAgentIds`, not duplicated here.
|
|
247
|
+
//
|
|
248
|
+
// Deterministic order. Returns a fresh array per call.
|
|
249
|
+
export function resolveDomainSpecialistAgentIds(archetype) {
|
|
250
|
+
const normalized = normalizeArchetype(archetype);
|
|
251
|
+
const specialists = DOMAIN_SPECIALIST_AGENT_IDS[normalized];
|
|
252
|
+
if (!Array.isArray(specialists)) return [];
|
|
253
|
+
return [...specialists];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// T25 / G7-gen: resolve the FULL roster for an archetype — the union of
|
|
257
|
+
// software-core agents (when applicable) plus domain specialists. This is
|
|
258
|
+
// the single function downstream callers (installers, dashboard tiles,
|
|
259
|
+
// `roster.synthesize` consumers) should reach for when they want the
|
|
260
|
+
// complete set of agents the generator believes a domain needs.
|
|
261
|
+
//
|
|
262
|
+
// Contract:
|
|
263
|
+
// - software archetype → all 4 SOFTWARE_CORE_AGENT_IDS, no specialists
|
|
264
|
+
// - book/content/design → only the domain specialists for that archetype
|
|
265
|
+
// - other archetypes → []
|
|
266
|
+
// - order is deterministic: software-core first, then domain specialists
|
|
267
|
+
// - no duplicates: even if a domain ever overlaps with a core id (it
|
|
268
|
+
// should not, by convention — see schemas.js), the union dedupes.
|
|
269
|
+
//
|
|
270
|
+
// The returned array is fresh per call so callers cannot mutate the
|
|
271
|
+
// canonical sources via the public surface.
|
|
272
|
+
export function resolveRosterForDomain(archetype) {
|
|
273
|
+
const normalized = normalizeArchetype(archetype);
|
|
274
|
+
const core = resolveSoftwareCoreAgentIds(normalized);
|
|
275
|
+
const specialists = resolveDomainSpecialistAgentIds(normalized);
|
|
276
|
+
const merged = [...core];
|
|
277
|
+
for (const id of specialists) {
|
|
278
|
+
if (!merged.includes(id)) merged.push(id);
|
|
279
|
+
}
|
|
280
|
+
return merged;
|
|
22
281
|
}
|
|
23
282
|
|
|
24
283
|
export function loadTeamTemplate(archetype) {
|
|
@@ -26,11 +285,126 @@ export function loadTeamTemplate(archetype) {
|
|
|
26
285
|
const path = join(FIXTURE_DIR, `${normalized}.json`);
|
|
27
286
|
const bundle = JSON.parse(readFileSync(path, 'utf8'));
|
|
28
287
|
assertValidTeamBundle(bundle);
|
|
288
|
+
|
|
289
|
+
// W1.5.B cross-validation gate. Read the matching T26 domain-template
|
|
290
|
+
// (if any) and cross-check that the fixture's `charter.roles[].name`
|
|
291
|
+
// set agrees with the template's `agent_ids`. Two sources of truth
|
|
292
|
+
// that SHOULD agree by construction (ADR W1.5 Option C "merge"); this
|
|
293
|
+
// gate makes future drift self-detect at load time rather than as a
|
|
294
|
+
// silent runtime miss far from the cause.
|
|
295
|
+
//
|
|
296
|
+
// Behaviour:
|
|
297
|
+
// - template file missing
|
|
298
|
+
// → e.g. `mixed` (deliberately template-free per ADR W1.5).
|
|
299
|
+
// Fixture is the sole source of truth; gate is a no-op.
|
|
300
|
+
// - template present but `agent_ids` empty
|
|
301
|
+
// → fall back to fixture as ground truth; emit a one-time
|
|
302
|
+
// warning so the unpopulated template is visible. W1.5.C
|
|
303
|
+
// populated research + business; if any other archetype ever
|
|
304
|
+
// lands empty in future, operators see it immediately.
|
|
305
|
+
// - template populated AND agrees with fixture role names
|
|
306
|
+
// (every fixture role name appears in template `agent_ids`)
|
|
307
|
+
// → silent pass; the canonical contract holds.
|
|
308
|
+
// - template populated AND disagrees
|
|
309
|
+
// → emit a one-time warning naming the drifting role(s) and the
|
|
310
|
+
// template ids they failed to match. Fail-loud at load time
|
|
311
|
+
// via `process.emitWarning` so CI logs and operators see the
|
|
312
|
+
// drift, but DON'T throw during the v1.5.1 transitional
|
|
313
|
+
// window where W1.5.E (fixture rename) and W1.5.B (this gate)
|
|
314
|
+
// ship in separate commits. Once W1.5.E lands and shipped
|
|
315
|
+
// fixtures are realigned, this warning never fires in the
|
|
316
|
+
// normal codepath; downstream may graduate the warning to a
|
|
317
|
+
// thrown error once the steady state is confirmed.
|
|
318
|
+
//
|
|
319
|
+
// The contract for callers: a non-throwing return ALWAYS means the
|
|
320
|
+
// fixture bundle is structurally valid (assertValidTeamBundle above);
|
|
321
|
+
// drift between fixture role names and the domain-template agent_ids
|
|
322
|
+
// surfaces as a one-time `IjfwTeamFixtureDrift` warning on stderr.
|
|
323
|
+
crossValidateAgainstDomainTemplate(normalized, bundle);
|
|
324
|
+
|
|
29
325
|
return structuredClone(bundle);
|
|
30
326
|
}
|
|
31
327
|
|
|
328
|
+
// W1.5.B: read the T26 domain-template for an archetype (if any) and
|
|
329
|
+
// compare it against the fixture bundle's role names. See
|
|
330
|
+
// loadTeamTemplate above for the contract and ADR W1.5 for the rationale.
|
|
331
|
+
function crossValidateAgainstDomainTemplate(archetype, bundle) {
|
|
332
|
+
const templatePath = join(DOMAIN_TEMPLATES_DIR, `${archetype}.json`);
|
|
333
|
+
if (!existsSync(templatePath)) {
|
|
334
|
+
// No T26 template for this archetype (e.g. `mixed` — deliberately
|
|
335
|
+
// template-free per ADR W1.5). Fixture is the sole source of truth.
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let template;
|
|
340
|
+
try {
|
|
341
|
+
template = JSON.parse(readFileSync(templatePath, 'utf8'));
|
|
342
|
+
} catch (err) {
|
|
343
|
+
// Unparseable template should not silently bypass the gate, but a
|
|
344
|
+
// parse error in the template is a separate failure mode from
|
|
345
|
+
// drift; emit a warning so the broken file is visible without
|
|
346
|
+
// breaking the generator for users whose fixture is structurally fine.
|
|
347
|
+
warnOnce(
|
|
348
|
+
`team-generator: domain-template "${archetype}.json" is unreadable (${err.message}); ` +
|
|
349
|
+
'falling back to fixture as ground truth.',
|
|
350
|
+
`parse:${archetype}`,
|
|
351
|
+
);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const templateIds = Array.isArray(template.agent_ids) ? template.agent_ids : [];
|
|
356
|
+
if (templateIds.length === 0) {
|
|
357
|
+
// ADR W1.5 step 5: T26 empty. Fall back to fixture as ground truth
|
|
358
|
+
// and emit a one-time warning so the gap is visible to operators.
|
|
359
|
+
warnOnce(
|
|
360
|
+
`team-generator: domain-template "${archetype}" has empty agent_ids — ` +
|
|
361
|
+
'falling back to fixture as ground truth.',
|
|
362
|
+
`empty:${archetype}`,
|
|
363
|
+
);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const fixtureRoleNames = (bundle.charter && Array.isArray(bundle.charter.roles))
|
|
368
|
+
? bundle.charter.roles.map((role) => role && role.name).filter(Boolean)
|
|
369
|
+
: [];
|
|
370
|
+
|
|
371
|
+
// The contract (ADR W1.5 Option C): every shipped-fixture role.name
|
|
372
|
+
// MUST appear in the template's agent_ids set. Drift is a load-time
|
|
373
|
+
// observability signal — emitted as a one-time warning so CI/operators
|
|
374
|
+
// see it, but NOT thrown during the v1.5.1 transitional window where
|
|
375
|
+
// W1.5.E (fixture rename) and W1.5.B (this gate) ship in separate
|
|
376
|
+
// commits. Steady state post-W1.5.E: this warning never fires; the
|
|
377
|
+
// gate becomes effectively-throw because mismatch can no longer exist.
|
|
378
|
+
const templateSet = new Set(templateIds);
|
|
379
|
+
const missing = fixtureRoleNames.filter((name) => !templateSet.has(name));
|
|
380
|
+
if (missing.length === 0) return;
|
|
381
|
+
|
|
382
|
+
warnOnce(
|
|
383
|
+
`team-generator: fixtures/team/${archetype}.json drifted from ` +
|
|
384
|
+
`domain-templates/${archetype}.json — role name(s) ${missing.map((m) => `"${m}"`).join(', ')} ` +
|
|
385
|
+
`not present in template agent_ids [${templateIds.join(', ')}]. ` +
|
|
386
|
+
'See ADR .planning/1.5.1/decisions/W1.5-canonical-source.md (Option C "merge"): ' +
|
|
387
|
+
'fixture roles MUST be a subset of the domain-template agent_ids. ' +
|
|
388
|
+
'W1.5.E (fixture rename) will close any remaining drift; until then ' +
|
|
389
|
+
'the fixture is treated as ground truth for the executable team bundle.',
|
|
390
|
+
`drift:${archetype}`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function warnOnce(message, key) {
|
|
395
|
+
if (_crossValidationWarned.has(key)) return;
|
|
396
|
+
_crossValidationWarned.add(key);
|
|
397
|
+
// process.emitWarning is the Node-canonical channel for non-fatal
|
|
398
|
+
// load-time signals — visible to CI logs, suppressible via
|
|
399
|
+
// --no-warnings, and includes a stack so the source is traceable.
|
|
400
|
+
process.emitWarning(message, { type: 'IjfwTeamFixtureDrift', code: 'IJFW_W1_5_B_DRIFT' });
|
|
401
|
+
}
|
|
402
|
+
|
|
32
403
|
export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
|
|
33
404
|
const root = resolve(projectRoot);
|
|
405
|
+
// F-FUN-1: `options.brief` flows through to detectTeamArchetype so a
|
|
406
|
+
// CLI / MCP caller can hand in a project brief and get a correct
|
|
407
|
+
// archetype before any filesystem signals exist.
|
|
34
408
|
const archetype = detectTeamArchetype(root, options);
|
|
35
409
|
const bundle = loadTeamTemplate(archetype);
|
|
36
410
|
const teamName = options.teamName || `${basename(root) || archetype}-team`;
|
|
@@ -56,13 +430,54 @@ export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
|
|
|
56
430
|
writeAtomic(workflowPath, `${JSON.stringify(bundle.workflow, null, 2)}\n`, { mode: 0o600 });
|
|
57
431
|
|
|
58
432
|
const agentFiles = [];
|
|
433
|
+
const writtenAgentNames = new Set();
|
|
59
434
|
for (const role of bundle.charter.roles) {
|
|
60
435
|
const agentPath = join(agentsDir, `${role.name}.md`);
|
|
61
436
|
writeAtomic(agentPath, renderAgent(role, bundle), { mode: 0o600 });
|
|
62
437
|
agentFiles.push(agentPath);
|
|
438
|
+
writtenAgentNames.add(role.name);
|
|
63
439
|
}
|
|
64
440
|
const codexAgents = syncCodexAgents(root, { bundle });
|
|
65
441
|
|
|
442
|
+
// T24 / G7-core: software-domain rosters always advertise the static
|
|
443
|
+
// software-core agent set. Non-software archetypes get an empty array
|
|
444
|
+
// (preserves the field on every return shape so callers don't need to
|
|
445
|
+
// null-check). The ids point to `claude/agents/<id>.md` in the IJFW
|
|
446
|
+
// install; the installer is responsible for placing the markdown.
|
|
447
|
+
const softwareCoreAgentIds = resolveSoftwareCoreAgentIds(archetype);
|
|
448
|
+
|
|
449
|
+
// T25 / G7-gen: domain-specific specialist agent ids. Same on-disk
|
|
450
|
+
// contract as the software-core ids — each id resolves to
|
|
451
|
+
// `claude/agents/<id>.md`. T25 returns the ids; T26 lands the matching
|
|
452
|
+
// markdown files. Until T26 ships, downstream installers should treat
|
|
453
|
+
// a missing file as "deploy stub" rather than fail-closed.
|
|
454
|
+
const domainSpecialistAgentIds = resolveDomainSpecialistAgentIds(archetype);
|
|
455
|
+
const rosterAgentIds = resolveRosterForDomain(archetype);
|
|
456
|
+
|
|
457
|
+
// W1.5.B: wire DOMAIN_SPECIALIST_AGENT_IDS through to file creation.
|
|
458
|
+
// Previously the canonical specialist ids were surfaced in the return
|
|
459
|
+
// value (`domainSpecialistAgentIds`, `rosterAgentIds`) but no matching
|
|
460
|
+
// `.md` files were written into `.ijfw/agents/` — so a swarm
|
|
461
|
+
// dispatcher resolving "the book domain specialists" by id got a list
|
|
462
|
+
// pointing at non-existent local files. This loop closes that gap by
|
|
463
|
+
// emitting a stub agent .md for every canonical specialist id that
|
|
464
|
+
// the fixture role-write loop above did NOT already cover. When
|
|
465
|
+
// fixture role names already match the canonical ids (the steady
|
|
466
|
+
// state post-W1.5.E), this loop is a no-op because `writtenAgentNames`
|
|
467
|
+
// already contains them.
|
|
468
|
+
//
|
|
469
|
+
// The stub references the canonical `claude/agents/<id>.md` so a
|
|
470
|
+
// swarm dispatcher resolving by id still gets a discoverable local
|
|
471
|
+
// entry; the installer is responsible for materialising the full
|
|
472
|
+
// agent spec.
|
|
473
|
+
for (const specialistId of domainSpecialistAgentIds) {
|
|
474
|
+
if (writtenAgentNames.has(specialistId)) continue;
|
|
475
|
+
const agentPath = join(agentsDir, `${specialistId}.md`);
|
|
476
|
+
writeAtomic(agentPath, renderSpecialistStub(specialistId, archetype, bundle), { mode: 0o600 });
|
|
477
|
+
agentFiles.push(agentPath);
|
|
478
|
+
writtenAgentNames.add(specialistId);
|
|
479
|
+
}
|
|
480
|
+
|
|
66
481
|
return {
|
|
67
482
|
ok: true,
|
|
68
483
|
archetype,
|
|
@@ -73,6 +488,9 @@ export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
|
|
|
73
488
|
workflowPath,
|
|
74
489
|
agentFiles,
|
|
75
490
|
codexAgents,
|
|
491
|
+
softwareCoreAgentIds,
|
|
492
|
+
domainSpecialistAgentIds,
|
|
493
|
+
rosterAgentIds,
|
|
76
494
|
};
|
|
77
495
|
}
|
|
78
496
|
|
|
@@ -105,7 +523,26 @@ export function readTeamAssembly(projectRoot = process.cwd()) {
|
|
|
105
523
|
|
|
106
524
|
function normalizeArchetype(value) {
|
|
107
525
|
const archetype = String(value || '').toLowerCase();
|
|
108
|
-
|
|
526
|
+
if (SUPPORTED_ARCHETYPES.has(archetype)) return archetype;
|
|
527
|
+
// F-FUN-1: canonicalize via the alias map BEFORE collapsing to 'mixed'.
|
|
528
|
+
// Without this, the detector's language-flavoured outputs (typescript,
|
|
529
|
+
// javascript, python, ...) and project-type-detector's 'unknown' all
|
|
530
|
+
// collapse to 'mixed', losing the strongest signal we have.
|
|
531
|
+
const aliased = ARCHETYPE_ALIASES.get(archetype);
|
|
532
|
+
if (aliased && SUPPORTED_ARCHETYPES.has(aliased)) return aliased;
|
|
533
|
+
return 'mixed';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// W1.5.B: stub renderer for canonical domain-specialist ids that the
|
|
537
|
+
// shipped fixture doesn't yet name as a `charter.roles[].name`. Keeps
|
|
538
|
+
// the `.ijfw/agents/<id>.md` directory in agreement with
|
|
539
|
+
// `domainSpecialistAgentIds` so downstream swarm dispatchers resolving
|
|
540
|
+
// by id always find a local file. The stub is intentionally minimal —
|
|
541
|
+
// the full agent spec lives in `claude/agents/<id>.md` and is deployed
|
|
542
|
+
// by the installer; this is a discovery breadcrumb, not a duplicated spec.
|
|
543
|
+
function renderSpecialistStub(specialistId, archetype, bundle) {
|
|
544
|
+
const archetypes = bundle.charter.project_archetypes.join(', ');
|
|
545
|
+
return `---\nname: ${specialistId}\nmodel: sonnet\neffort: medium\ndescription: ${specialistId} — canonical ${archetype} domain specialist (T26 domain-template).\nallowed-tools: Read, Write, Edit, Bash\n---\n\n# ${specialistId}\n\nCanonical domain specialist for ${archetypes} projects.\n\nFull agent specification: claude/agents/${specialistId}.md (deployed by the IJFW installer).\n\nThis stub exists so swarm dispatchers resolving by id find a local entry; the canonical spec is the source of truth.\n\nRecord claims, findings, blockers, and decisions in .ijfw/blackboard/ when swarm execution is active.\n`;
|
|
109
546
|
}
|
|
110
547
|
|
|
111
548
|
function renderAgent(role, bundle) {
|