rcf-lite 0.0.1 → 0.7.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/CHANGELOG.md +288 -0
- package/LICENSE +202 -0
- package/README.md +92 -4
- package/bin/rcf-verify.js +122 -0
- package/bin/rcf.js +174 -0
- package/bin/view-supervisor-child.mjs +14 -0
- package/fixtures/canary-manifest.json +103 -0
- package/guidance/README.md +25 -0
- package/guidance/build-cycle-playbook.md +492 -0
- package/guidance/build-cycle.md +40 -0
- package/guidance/document-model.md +45 -0
- package/guidance/elicitation-playbook.md +442 -0
- package/guidance/harness-template.md +246 -0
- package/guidance/managed/README.md +63 -0
- package/guidance/managed/agent-instructions-block.hash +1 -0
- package/guidance/managed/agent-instructions-block.md +220 -0
- package/guidance/managed/legacy-fragment-hashes.json +22 -0
- package/guidance/manifest.json +21 -0
- package/guidance/overview.md +48 -0
- package/guidance/persona-programme.md +44 -0
- package/package.json +69 -8
- package/rcf/adrs/adr-001.json +25 -0
- package/rcf/adrs/adr-002.json +25 -0
- package/rcf/adrs/adr-003.json +25 -0
- package/rcf/adrs/adr-004.json +20 -0
- package/rcf/adrs/adr-005.json +20 -0
- package/rcf/adrs/adr-006.json +25 -0
- package/rcf/adrs/adr-007.json +25 -0
- package/rcf/adrs/adr-008.json +25 -0
- package/rcf/adrs/adr-009.json +25 -0
- package/rcf/build-sequence.json +11 -0
- package/rcf/code-nodes/cn-001.json +14 -0
- package/rcf/code-nodes/cn-002.json +14 -0
- package/rcf/code-nodes/cn-003.json +15 -0
- package/rcf/code-nodes/cn-004.json +17 -0
- package/rcf/code-nodes/cn-005.json +19 -0
- package/rcf/code-nodes/cn-006.json +17 -0
- package/rcf/code-nodes/cn-007.json +16 -0
- package/rcf/code-nodes/cn-008.json +19 -0
- package/rcf/code-nodes/cn-009.json +16 -0
- package/rcf/code-nodes/cn-010.json +18 -0
- package/rcf/code-nodes/cn-011.json +18 -0
- package/rcf/code-nodes/cn-012.json +18 -0
- package/rcf/code-nodes/cn-013.json +19 -0
- package/rcf/code-nodes/cn-014.json +16 -0
- package/rcf/code-nodes/cn-015.json +16 -0
- package/rcf/code-nodes/cn-016.json +16 -0
- package/rcf/code-nodes/cn-017.json +16 -0
- package/rcf/code-nodes/cn-018.json +15 -0
- package/rcf/code-nodes/cn-019.json +16 -0
- package/rcf/code-nodes/cn-020.json +16 -0
- package/rcf/code-nodes/cn-021.json +16 -0
- package/rcf/code-nodes/cn-022.json +16 -0
- package/rcf/code-nodes/cn-023.json +16 -0
- package/rcf/code-nodes/cn-024.json +16 -0
- package/rcf/code-nodes/cn-025.json +16 -0
- package/rcf/code-nodes/cn-026.json +16 -0
- package/rcf/code-nodes/cn-027.json +16 -0
- package/rcf/code-nodes/cn-028.json +16 -0
- package/rcf/code-nodes/cn-029.json +19 -0
- package/rcf/code-nodes/cn-030.json +14 -0
- package/rcf/code-nodes/cn-031.json +14 -0
- package/rcf/code-nodes/cn-032.json +14 -0
- package/rcf/code-nodes/cn-033.json +14 -0
- package/rcf/code-nodes/cn-034.json +14 -0
- package/rcf/code-nodes/cn-035.json +14 -0
- package/rcf/code-nodes/cn-036.json +14 -0
- package/rcf/code-nodes/cn-037.json +14 -0
- package/rcf/code-nodes/cn-038.json +14 -0
- package/rcf/code-nodes/cn-039.json +14 -0
- package/rcf/code-nodes/cn-040.json +14 -0
- package/rcf/code-nodes/cn-041.json +14 -0
- package/rcf/code-nodes/cn-042.json +14 -0
- package/rcf/code-nodes/cn-043.json +14 -0
- package/rcf/code-nodes/cn-044.json +14 -0
- package/rcf/code-nodes/cn-045.json +14 -0
- package/rcf/code-nodes/cn-046.json +14 -0
- package/rcf/code-nodes/cn-047.json +14 -0
- package/rcf/code-nodes/cn-048.json +14 -0
- package/rcf/code-nodes/cn-049.json +14 -0
- package/rcf/code-nodes/cn-050.json +14 -0
- package/rcf/code-nodes/cn-051.json +14 -0
- package/rcf/code-nodes/cn-052.json +14 -0
- package/rcf/code-nodes/cn-053.json +14 -0
- package/rcf/code-nodes/cn-054.json +14 -0
- package/rcf/code-nodes/cn-055.json +14 -0
- package/rcf/code-nodes/cn-056.json +14 -0
- package/rcf/code-nodes/cn-057.json +14 -0
- package/rcf/fbs/fbs-001.json +49 -0
- package/rcf/fbs/fbs-002.json +42 -0
- package/rcf/fbs/fbs-003.json +37 -0
- package/rcf/fbs/fbs-004.json +39 -0
- package/rcf/fbs/fbs-005.json +38 -0
- package/rcf/fbs/fbs-006.json +48 -0
- package/rcf/fbs/fbs-007.json +39 -0
- package/rcf/fbs/fbs-008.json +40 -0
- package/rcf/fbs/fbs-009.json +36 -0
- package/rcf/fbs/fbs-010.json +41 -0
- package/rcf/fbs/fbs-011.json +36 -0
- package/rcf/fbs/fbs-012.json +46 -0
- package/rcf/fbs/fbs-013.json +42 -0
- package/rcf/fbs/fbs-014.json +49 -0
- package/rcf/fbs/fbs-015.json +32 -0
- package/rcf/manifest.json +17 -0
- package/rcf/prd.json +47 -0
- package/rcf/requirements/req-001.json +19 -0
- package/rcf/requirements/req-002.json +19 -0
- package/rcf/requirements/req-003.json +19 -0
- package/rcf/requirements/req-004.json +19 -0
- package/rcf/requirements/req-005.json +19 -0
- package/rcf/requirements/req-006.json +19 -0
- package/rcf/requirements/req-007.json +19 -0
- package/rcf/requirements/req-008.json +19 -0
- package/rcf/requirements/req-009.json +19 -0
- package/rcf/tacs/tac-001.json +45 -0
- package/rcf/tacs/tac-002.json +109 -0
- package/rcf/tacs/tac-003.json +40 -0
- package/rcf/tacs/tac-004.json +51 -0
- package/rcf/tacs/tac-005.json +52 -0
- package/rcf/tacs/tac-006.json +104 -0
- package/rcf/tacs/tac-007.json +38 -0
- package/rcf/tacs/tac-008.json +51 -0
- package/rcf/tad.json +51 -0
- package/rcf/test-suites/PENDING.md +23 -0
- package/rcf/test-suites/ts-001.json +38 -0
- package/rcf/test-suites/ts-002.json +38 -0
- package/rcf/test-suites/ts-003.json +43 -0
- package/rcf/test-suites/ts-004.json +44 -0
- package/rcf/test-suites/ts-005.json +30 -0
- package/rcf/test-suites/ts-006.json +36 -0
- package/rcf/test-suites/ts-007.json +43 -0
- package/rcf/test-suites/ts-008.json +37 -0
- package/rcf/test-suites/ts-009.json +38 -0
- package/rcf/test-suites/ts-010.json +38 -0
- package/rcf/test-suites/ts-011.json +44 -0
- package/rcf/test-suites/ts-012.json +36 -0
- package/rcf/test-suites/ts-013.json +38 -0
- package/rcf/test-suites/ts-014.json +38 -0
- package/rcf/test-suites/ts-015.json +38 -0
- package/rcf/test-suites/ts-016.json +37 -0
- package/rcf/test-suites/ts-017.json +38 -0
- package/rcf/test-suites/ts-018.json +38 -0
- package/rcf/test-suites/ts-019.json +37 -0
- package/rcf/test-suites/ts-020.json +46 -0
- package/rcf/test-suites/ts-021.json +46 -0
- package/rcf/test-suites/ts-022.json +46 -0
- package/rcf/test-suites/ts-023.json +46 -0
- package/rcf/test-suites/ts-024.json +46 -0
- package/rcf/test-suites/ts-025.json +52 -0
- package/rcf/user-stories/us-101.json +40 -0
- package/rcf/user-stories/us-102.json +40 -0
- package/rcf/user-stories/us-201.json +40 -0
- package/rcf/user-stories/us-202.json +40 -0
- package/rcf/user-stories/us-203.json +32 -0
- package/rcf/user-stories/us-301.json +40 -0
- package/rcf/user-stories/us-302.json +40 -0
- package/rcf/user-stories/us-303.json +40 -0
- package/rcf/user-stories/us-304.json +40 -0
- package/rcf/user-stories/us-401.json +40 -0
- package/rcf/user-stories/us-402.json +40 -0
- package/rcf/user-stories/us-403.json +40 -0
- package/rcf/user-stories/us-501.json +40 -0
- package/rcf/user-stories/us-502.json +40 -0
- package/rcf/user-stories/us-503.json +40 -0
- package/rcf/user-stories/us-601.json +40 -0
- package/rcf/user-stories/us-602.json +40 -0
- package/rcf/user-stories/us-701.json +40 -0
- package/rcf/user-stories/us-702.json +40 -0
- package/rcf/user-stories/us-801.json +49 -0
- package/rcf/user-stories/us-802.json +49 -0
- package/rcf/user-stories/us-803.json +49 -0
- package/rcf/user-stories/us-804.json +49 -0
- package/rcf/user-stories/us-805.json +49 -0
- package/rcf/user-stories/us-901.json +40 -0
- package/src/.gitkeep +0 -0
- package/src/browser-verify/auth-smoke.js +109 -0
- package/src/browser-verify/index.js +29 -0
- package/src/browser-verify/invariants.js +336 -0
- package/src/browser-verify/manifest-writer.js +189 -0
- package/src/browser-verify/runner.js +171 -0
- package/src/build/bundle.js +198 -0
- package/src/build/formatters/json.js +18 -0
- package/src/build/formatters/markdown.js +406 -0
- package/src/build/index.js +14 -0
- package/src/build/mark.js +177 -0
- package/src/build/queue.js +285 -0
- package/src/cli/browser-verify.js +231 -0
- package/src/cli/build.js +584 -0
- package/src/cli/coverage.js +219 -0
- package/src/cli/create.js +355 -0
- package/src/cli/delete.js +127 -0
- package/src/cli/design.js +353 -0
- package/src/cli/doctor.js +548 -0
- package/src/cli/fbs.js +171 -0
- package/src/cli/finalise.js +377 -0
- package/src/cli/guidance.js +206 -0
- package/src/cli/help.js +156 -0
- package/src/cli/impact.js +119 -0
- package/src/cli/init.js +282 -0
- package/src/cli/intake.js +153 -0
- package/src/cli/link.js +128 -0
- package/src/cli/mcp.js +160 -0
- package/src/cli/preflight.js +220 -0
- package/src/cli/read.js +162 -0
- package/src/cli/req-baseline.js +269 -0
- package/src/cli/req-classify.js +135 -0
- package/src/cli/review.js +295 -0
- package/src/cli/test-suite.js +216 -0
- package/src/cli/trace.js +172 -0
- package/src/cli/ui-baseline.js +292 -0
- package/src/cli/ui-classify.js +108 -0
- package/src/cli/update.js +197 -0
- package/src/cli/validate.js +168 -0
- package/src/cli/view.js +452 -0
- package/src/core/baseline-catalog/data/auth.json +42 -0
- package/src/core/baseline-catalog/data/http-api.json +42 -0
- package/src/core/baseline-catalog/data/notifications.json +33 -0
- package/src/core/baseline-catalog/data/persistence.json +33 -0
- package/src/core/baseline-catalog/data/web-ui.json +60 -0
- package/src/core/baseline-catalog/index.js +121 -0
- package/src/core/errors/index.js +167 -0
- package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
- package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
- package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
- package/src/core/isolation/index.js +60 -0
- package/src/core/mcp/framing.js +103 -0
- package/src/core/mcp/index.js +8 -0
- package/src/core/mcp/server.js +228 -0
- package/src/core/patterns/register-canary.js +209 -0
- package/src/core/patterns/req-shapes.js +158 -0
- package/src/core/patterns/services.js +358 -0
- package/src/core/patterns/ui-shapes.js +166 -0
- package/src/core/store/cn-resolve.js +134 -0
- package/src/core/store/derive-deps.js +93 -0
- package/src/core/store/ids.js +78 -0
- package/src/core/store/index.js +20 -0
- package/src/core/store/init.js +255 -0
- package/src/core/store/loader.js +211 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +898 -0
- package/src/core/store/writer.js +1849 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +129 -0
- package/src/finalise/index.js +17 -0
- package/src/finalise/ingest.js +119 -0
- package/src/finalise/install.js +119 -0
- package/src/finalise/ship-without-verified.js +131 -0
- package/src/finalise/spawn.js +84 -0
- package/src/intake/fidelity.js +105 -0
- package/src/intake/index.js +6 -0
- package/src/intake/manifest-writer.js +100 -0
- package/src/intake/orchestrator.js +138 -0
- package/src/intake/validate.js +80 -0
- package/src/mcp/map-errors.js +131 -0
- package/src/mcp/prompts.js +49 -0
- package/src/mcp/resources.js +244 -0
- package/src/mcp/tools.js +1204 -0
- package/src/preflight/design-shapes.js +185 -0
- package/src/preflight/index.js +90 -0
- package/src/preflight/manifest-writer.js +264 -0
- package/src/preflight/scanner.js +206 -0
- package/src/preflight/secrets.js +134 -0
- package/src/preflight/session.js +246 -0
- package/src/query/attestation.js +285 -0
- package/src/query/coverage.js +308 -0
- package/src/query/formatters/json.js +21 -0
- package/src/query/formatters/mermaid.js +209 -0
- package/src/query/formatters/table.js +203 -0
- package/src/query/impact.js +173 -0
- package/src/query/index.js +9 -0
- package/src/query/trace.js +345 -0
- package/src/register-canary/fixture-loader.js +87 -0
- package/src/register-canary/index.js +10 -0
- package/src/register-canary/record-writer.js +132 -0
- package/src/register-canary/runner.js +156 -0
- package/src/req-baseline/gate.js +86 -0
- package/src/req-baseline/index.js +27 -0
- package/src/req-baseline/open-candidates.js +143 -0
- package/src/req-baseline/opt-out.js +195 -0
- package/src/req-baseline/sweep.js +230 -0
- package/src/req-detection/classifier.js +181 -0
- package/src/req-detection/index.js +9 -0
- package/src/req-detection/persist.js +55 -0
- package/src/review/index.js +325 -0
- package/src/review/mutation.js +117 -0
- package/src/review/ui-baseline-drift.js +138 -0
- package/src/server/index.js +178 -0
- package/src/server/routes.js +110 -0
- package/src/server/sse.js +118 -0
- package/src/setup/agent-setup.js +362 -0
- package/src/setup/identity-seed.js +104 -0
- package/src/setup/knowledge-seed.js +123 -0
- package/src/setup/managed-block.js +193 -0
- package/src/setup/managed-gitignore.js +166 -0
- package/src/setup/managed-markers.js +49 -0
- package/src/ui-baseline/defaults.js +119 -0
- package/src/ui-baseline/index.js +25 -0
- package/src/ui-baseline/manifest-writer.js +282 -0
- package/src/ui-baseline/session.js +178 -0
- package/src/ui-detection/classifier.js +192 -0
- package/src/verify/chain/index.js +190 -0
- package/src/verify/cli/cleanup.js +61 -0
- package/src/verify/cli/help.js +56 -0
- package/src/verify/cli/mcp.js +98 -0
- package/src/verify/cli/provision.js +71 -0
- package/src/verify/cli/report.js +71 -0
- package/src/verify/cli/run.js +155 -0
- package/src/verify/engine/brief.js +87 -0
- package/src/verify/engine/index.js +177 -0
- package/src/verify/engine/launcher.js +307 -0
- package/src/verify/mcp/tools.js +107 -0
- package/src/verify/profile/index.js +146 -0
- package/src/verify/provision/index.js +256 -0
- package/src/verify/report/index.js +139 -0
- package/src/verify/report/renderer.js +118 -0
- package/src/verify/verdict/index.js +246 -0
- package/src/view/doc-renderers/adr.js +44 -0
- package/src/view/doc-renderers/build-sequence.js +40 -0
- package/src/view/doc-renderers/fbs.js +128 -0
- package/src/view/doc-renderers/helpers.js +159 -0
- package/src/view/doc-renderers/index.js +12 -0
- package/src/view/doc-renderers/prd.js +45 -0
- package/src/view/doc-renderers/req.js +43 -0
- package/src/view/doc-renderers/tac.js +38 -0
- package/src/view/doc-renderers/tad.js +74 -0
- package/src/view/doc-renderers/test-suite.js +45 -0
- package/src/view/doc-renderers/user-story.js +63 -0
- package/src/view/html-page.js +462 -0
- package/src/view/index.js +63 -0
- package/src/view/live-client.js +338 -0
- package/src/view/mermaid-diagram.js +178 -0
- package/src/view/style.css +735 -0
- package/src/view/tree-model.js +152 -0
- package/src/view/vendored/mermaid.min.js +2607 -0
- package/src/view-supervisor/index.js +26 -0
- package/src/view-supervisor/logs.js +32 -0
- package/src/view-supervisor/manifest-writer.js +178 -0
- package/src/view-supervisor/persist-until.js +85 -0
- package/src/view-supervisor/supervisor.js +276 -0
- package/src/watch/index.js +152 -0
package/src/cli/init.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// `rcf init` subcommand handler. Interactive by default when stdin +
|
|
2
|
+
// stdout are TTYs and --non-interactive is not set. Init is a bootstrap,
|
|
3
|
+
// not an elicitation session: it asks only for the project name and
|
|
4
|
+
// seeds a fully placeholder document tree - the same tree the
|
|
5
|
+
// non-interactive path produces - which the agent fills in during
|
|
6
|
+
// elicitation. It deliberately does NOT ask the user to name a first
|
|
7
|
+
// requirement / user story / problem statement up front (operator review
|
|
8
|
+
// 2026-07-16, comment 2: a product owner does not know those yet, and
|
|
9
|
+
// asking makes them freeze or type junk). Zero deps; prompt via
|
|
10
|
+
// node:readline/promises.
|
|
11
|
+
//
|
|
12
|
+
// Theme 1 (E2E matrix 2026-07-06-003): init is the full pre-session
|
|
13
|
+
// bootstrap. After scaffolding the tree it also (1) writes/merges the
|
|
14
|
+
// project-root .mcp.json rcf server entry and (2) writes the guidance
|
|
15
|
+
// fragment into CLAUDE.md / AGENTS.md inside rcf marker comments -
|
|
16
|
+
// the project is wired BEFORE the agent session starts. Re-running
|
|
17
|
+
// init on an existing tree leaves the tree alone and refreshes the
|
|
18
|
+
// wiring (idempotent). --no-agent-setup skips the wiring and prints
|
|
19
|
+
// the manual instructions instead.
|
|
20
|
+
|
|
21
|
+
import { parseArgs } from 'node:util';
|
|
22
|
+
import { createInterface } from 'node:readline/promises';
|
|
23
|
+
|
|
24
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
|
|
27
|
+
import { initProject } from '#core/store/init.js';
|
|
28
|
+
import {
|
|
29
|
+
loadManagedBlock,
|
|
30
|
+
manualSetupInstructions,
|
|
31
|
+
writeAgentInstructions,
|
|
32
|
+
writeMcpConfig,
|
|
33
|
+
} from '../setup/agent-setup.js';
|
|
34
|
+
import { writeKnowledgeSeed } from '../setup/knowledge-seed.js';
|
|
35
|
+
import { writeIdentityTemplate } from '../setup/identity-seed.js';
|
|
36
|
+
import {
|
|
37
|
+
composeGitignoreBlock,
|
|
38
|
+
computeGitignoreBlockHash,
|
|
39
|
+
extractGitignoreBlock,
|
|
40
|
+
GITIGNORE_MARKER_BEGIN,
|
|
41
|
+
GITIGNORE_MARKER_END,
|
|
42
|
+
} from '../setup/managed-gitignore.js';
|
|
43
|
+
import { hashInnerContent } from '../setup/managed-block.js';
|
|
44
|
+
|
|
45
|
+
const OPTION_SPEC = {
|
|
46
|
+
'project-name': { type: 'string' },
|
|
47
|
+
'non-interactive': { type: 'boolean' },
|
|
48
|
+
'no-agent-setup': { type: 'boolean' },
|
|
49
|
+
quiet: { type: 'boolean' },
|
|
50
|
+
help: { type: 'boolean' },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const HELP = `Usage: rcf init [options]
|
|
54
|
+
|
|
55
|
+
Scaffolds the rcf/ tree, registers the MCP server in the project-root
|
|
56
|
+
.mcp.json and writes the agent-instructions fragment into both CLAUDE.md
|
|
57
|
+
and AGENTS.md (an existing instructions file is refreshed in place) -
|
|
58
|
+
the full pre-session bootstrap. Interactive runs prompt only for the
|
|
59
|
+
project name; everything else is seeded as placeholders for the agent
|
|
60
|
+
to elicit. Re-running on an existing project leaves the tree alone and
|
|
61
|
+
refreshes the wiring.
|
|
62
|
+
|
|
63
|
+
Options:
|
|
64
|
+
--project-name <name> Project name (required for --non-interactive)
|
|
65
|
+
--non-interactive Skip prompts; use seed values (default when
|
|
66
|
+
not on a TTY or when piped)
|
|
67
|
+
--no-agent-setup Scaffold the tree only; print the manual
|
|
68
|
+
harness-wiring instructions instead
|
|
69
|
+
--quiet Suppress non-error stdout
|
|
70
|
+
--help Print this help
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string[]} argv - argv slice after `init`
|
|
75
|
+
* @param {object} [deps]
|
|
76
|
+
* @returns {Promise<number>}
|
|
77
|
+
*/
|
|
78
|
+
export async function main(argv, deps = {}) {
|
|
79
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
80
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
81
|
+
const stdin = deps.stdin ?? process.stdin;
|
|
82
|
+
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
88
|
+
stderr.write(HELP);
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
const flags = parsed.values;
|
|
92
|
+
if (flags.help) {
|
|
93
|
+
stdout.write(HELP);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
97
|
+
const agentSetup = !flags['no-agent-setup'];
|
|
98
|
+
const forceNonInteractive = Boolean(flags['non-interactive']);
|
|
99
|
+
const isTty = Boolean(stdout.isTTY && stdin.isTTY);
|
|
100
|
+
const interactive = !forceNonInteractive && isTty;
|
|
101
|
+
|
|
102
|
+
let projectName = flags['project-name'];
|
|
103
|
+
let seed = null;
|
|
104
|
+
|
|
105
|
+
if (interactive) {
|
|
106
|
+
// Init asks only for the project name. Everything else in the tree
|
|
107
|
+
// is seeded as a placeholder for the agent to elicit - see the file
|
|
108
|
+
// header. `seed` stays null so interactive and non-interactive
|
|
109
|
+
// produce the identical placeholder tree.
|
|
110
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
111
|
+
try {
|
|
112
|
+
projectName = projectName ?? (await rl.question('Project name: ')).trim();
|
|
113
|
+
if (!projectName) projectName = 'New RCF Project';
|
|
114
|
+
} finally {
|
|
115
|
+
rl.close();
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
if (!projectName) {
|
|
119
|
+
stderr.write('[error] usage --project-name is required in non-interactive mode\n');
|
|
120
|
+
stderr.write(HELP);
|
|
121
|
+
return 2;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const result = await initProject({ projectRoot: cwd, projectName, seed });
|
|
126
|
+
const treeExists = Boolean(result && 'kind' in result && result.kind === 'usage'
|
|
127
|
+
&& /already exists/.test(result.message));
|
|
128
|
+
if (result && 'kind' in result && result.kind === 'usage' && !treeExists) {
|
|
129
|
+
stderr.write(`[error] usage ${result.message}\n`);
|
|
130
|
+
return 2;
|
|
131
|
+
}
|
|
132
|
+
if (result && 'kind' in result && !treeExists) {
|
|
133
|
+
stderr.write(`[error] ${result.kind} ${result.message}\n`);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
if (treeExists && !agentSetup) {
|
|
137
|
+
// Nothing to do at all: tree present, wiring explicitly skipped.
|
|
138
|
+
stderr.write(`[error] usage ${result.message}\n`);
|
|
139
|
+
return 2;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --no-agent-setup: scaffold the tree only, print the manual wiring
|
|
143
|
+
// steps. (A tree-already-present + opt-out combination has already
|
|
144
|
+
// been refused above, so this branch is always a fresh scaffold.)
|
|
145
|
+
if (!agentSetup) {
|
|
146
|
+
if (!flags.quiet) {
|
|
147
|
+
stdout.write('Set up the RCF document chain under rcf/.\n');
|
|
148
|
+
stdout.write(`${manualSetupInstructions()}\n`);
|
|
149
|
+
}
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Step 1: .mcp.json (merge; never clobber other servers / unknown keys).
|
|
154
|
+
const mcpResult = await writeMcpConfig({ projectRoot: cwd });
|
|
155
|
+
if (mcpResult && 'kind' in mcpResult && 'message' in mcpResult) {
|
|
156
|
+
stderr.write(`[error] ${mcpResult.kind} ${mcpResult.message}\n`);
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Step 2: agent-instructions managed block inside rcf markers (idempotent).
|
|
161
|
+
// Source is the 0.6.0 canonical asset, not the harness-template fenced
|
|
162
|
+
// fragment (which is now regenerated from the same canonical text at
|
|
163
|
+
// package build time via scripts/gen-managed-artefacts.mjs).
|
|
164
|
+
const fragment = await loadManagedBlock();
|
|
165
|
+
if (typeof fragment !== 'string') {
|
|
166
|
+
stderr.write(`[error] ${fragment.kind} ${fragment.message}\n`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
const instrResult = await writeAgentInstructions({ projectRoot: cwd, fragment });
|
|
170
|
+
|
|
171
|
+
// Step 3: knowledge space (§3, AC-2.1..AC-2.3). Idempotent per file.
|
|
172
|
+
const knowledgeResult = await writeKnowledgeSeed({ projectRoot: cwd });
|
|
173
|
+
// Step 4: identity template (§5, AC-4.1..AC-4.2). Idempotent.
|
|
174
|
+
const identityResult = await writeIdentityTemplate({ projectRoot: cwd });
|
|
175
|
+
// Step 5: managed .gitignore block (§4, AC-3.1..AC-3.2). Written or
|
|
176
|
+
// refreshed in place; operator content outside preserved byte-for-byte.
|
|
177
|
+
const gitignoreResult = await ensureManagedGitignore({ projectRoot: cwd });
|
|
178
|
+
|
|
179
|
+
// High-level completion summary: what was set up and what to do next -
|
|
180
|
+
// not a developer file list (operator review 2026-07-16, comment 3a).
|
|
181
|
+
if (!flags.quiet) {
|
|
182
|
+
let mcpDesc;
|
|
183
|
+
if (mcpResult.action === 'kept') mcpDesc = 'already registered in .mcp.json (kept)';
|
|
184
|
+
else if (mcpResult.action === 'merged') mcpDesc = 'registered in .mcp.json (merged with your existing servers)';
|
|
185
|
+
else mcpDesc = 'registered in .mcp.json';
|
|
186
|
+
|
|
187
|
+
const instrFiles = instrResult.writes.map((w) => w.file).join(' and ');
|
|
188
|
+
const actions = new Set(instrResult.writes.map((w) => w.action));
|
|
189
|
+
let instrVerb;
|
|
190
|
+
if (actions.size === 1 && actions.has('created')) instrVerb = 'written to';
|
|
191
|
+
else if (actions.has('replaced')) instrVerb = 'refreshed in';
|
|
192
|
+
else instrVerb = 'updated in';
|
|
193
|
+
|
|
194
|
+
if (treeExists) {
|
|
195
|
+
stdout.write('RCF project already set up here - document chain left untouched, agent wiring refreshed.\n');
|
|
196
|
+
} else {
|
|
197
|
+
stdout.write('RCF project created.\n');
|
|
198
|
+
stdout.write(' Document chain scaffolded under rcf/ - PRD, requirement, story, acceptance criterion, architecture and build-sequence placeholders for your agent to fill in.\n');
|
|
199
|
+
}
|
|
200
|
+
stdout.write(` MCP server ${mcpDesc}.\n`);
|
|
201
|
+
stdout.write(` Agent instructions ${instrVerb} ${instrFiles}.\n`);
|
|
202
|
+
stdout.write(` Knowledge space ${knowledgeVerbAndTarget(knowledgeResult)}.\n`);
|
|
203
|
+
stdout.write(` Operator profile ${identityVerbAndTarget(identityResult)}.\n`);
|
|
204
|
+
stdout.write(` Gitignore ${gitignoreVerbAndTarget(gitignoreResult)}.\n`);
|
|
205
|
+
stdout.write('\nNext: start your agent session in this directory and tell it what you want to build. '
|
|
206
|
+
+ 'It elicits the requirements and drives the build from there - you do not fill in the document chain by hand.\n');
|
|
207
|
+
}
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function knowledgeVerbAndTarget(result) {
|
|
212
|
+
const created = result.writes.filter((w) => w.action === 'created').length;
|
|
213
|
+
if (created === 0) return 'left as-is at rcf/knowledge/ (all managed files already present)';
|
|
214
|
+
if (created === result.writes.length) return 'seeded at rcf/knowledge/ (write what you learn; grep before asking)';
|
|
215
|
+
return `refreshed at rcf/knowledge/ (${created} new managed files)`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function identityVerbAndTarget(result) {
|
|
219
|
+
if (result.action === 'kept') return 'left as-is at rcf/.identity/profile.md (already present)';
|
|
220
|
+
return 'template at rcf/.identity/profile.md (gitignored by default; fill in what is useful)';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function gitignoreVerbAndTarget(result) {
|
|
224
|
+
if (result.action === 'noop') return 'managed block already up to date in .gitignore';
|
|
225
|
+
if (result.action === 'created') return 'managed block written to .gitignore (rcf/.identity/ ignored)';
|
|
226
|
+
if (result.action === 'appended') return 'managed block appended to existing .gitignore (rcf/.identity/ ignored)';
|
|
227
|
+
return 'managed block refreshed in .gitignore (rcf/.identity/ ignored)';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Ensure the project-root `.gitignore` carries the current managed
|
|
232
|
+
* block. Writes/refreshes in place; operator entries outside markers
|
|
233
|
+
* preserved byte-for-byte. Idempotent when block is already current.
|
|
234
|
+
*
|
|
235
|
+
* @param {object} args
|
|
236
|
+
* @param {string} args.projectRoot
|
|
237
|
+
* @returns {Promise<{ action: 'created' | 'appended' | 'replaced' | 'noop' }>}
|
|
238
|
+
*/
|
|
239
|
+
async function ensureManagedGitignore({ projectRoot }) {
|
|
240
|
+
const path = join(projectRoot, '.gitignore');
|
|
241
|
+
let existing;
|
|
242
|
+
try {
|
|
243
|
+
existing = await readFile(path, 'utf8');
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code !== 'ENOENT') throw err;
|
|
246
|
+
await writeFile(path, composeGitignoreBlock(), 'utf8');
|
|
247
|
+
return { action: 'created' };
|
|
248
|
+
}
|
|
249
|
+
const located = extractGitignoreBlock(existing);
|
|
250
|
+
const composed = composeGitignoreBlock();
|
|
251
|
+
if (!located) {
|
|
252
|
+
// No managed block yet - append with a blank-line separator.
|
|
253
|
+
const beginCount = countLocal(existing, GITIGNORE_MARKER_BEGIN);
|
|
254
|
+
const endCount = countLocal(existing, GITIGNORE_MARKER_END);
|
|
255
|
+
if (beginCount !== 0 || endCount !== 0) {
|
|
256
|
+
// Structurally malformed - leave alone; doctor will surface the
|
|
257
|
+
// drift and the operator repairs by hand.
|
|
258
|
+
return { action: 'noop' };
|
|
259
|
+
}
|
|
260
|
+
const sep = existing.length === 0 ? '' : (existing.endsWith('\n') ? '\n' : '\n\n');
|
|
261
|
+
await writeFile(path, `${existing}${sep}${composed}`, 'utf8');
|
|
262
|
+
return { action: 'appended' };
|
|
263
|
+
}
|
|
264
|
+
const innerHash = hashInnerContent(located.innerText);
|
|
265
|
+
const expected = computeGitignoreBlockHash();
|
|
266
|
+
if (innerHash === expected) return { action: 'noop' };
|
|
267
|
+
const next = existing.slice(0, located.beginIndex) + composed + existing.slice(located.endIndex);
|
|
268
|
+
await writeFile(path, next, 'utf8');
|
|
269
|
+
return { action: 'replaced' };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function countLocal(haystack, needle) {
|
|
273
|
+
if (!needle) return 0;
|
|
274
|
+
let n = 0;
|
|
275
|
+
let i = 0;
|
|
276
|
+
while (true) {
|
|
277
|
+
const at = haystack.indexOf(needle, i);
|
|
278
|
+
if (at < 0) return n;
|
|
279
|
+
n += 1;
|
|
280
|
+
i = at + needle.length;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// `rcf intake` subcommand handler
|
|
2
|
+
// (elicitation-and-playbook-hardening-0.7.0-spec §6).
|
|
3
|
+
//
|
|
4
|
+
// Variable-fidelity intake stage. Classifies the supplied artefacts by
|
|
5
|
+
// fidelity (none | napkin | briefLight | briefStrong | prd | prdPlusTad
|
|
6
|
+
// per spec §3.7), applies elicitation-integrity discipline (§6.4 phase 2
|
|
7
|
+
// scans: impliedButNotStated, contradiction, missingLoadBearingConstraint),
|
|
8
|
+
// records validation findings, and writes an intakeClassification record
|
|
9
|
+
// to the manifest. Elicitation of gaps handed off to the standard
|
|
10
|
+
// playbook §3-§7 (this verb closes at phase 2).
|
|
11
|
+
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { parseArgs } from 'node:util';
|
|
14
|
+
|
|
15
|
+
import { walkTree } from '#core/store';
|
|
16
|
+
|
|
17
|
+
import { findProjectRoot } from '../view/index.js';
|
|
18
|
+
import {
|
|
19
|
+
runIntakePhases,
|
|
20
|
+
writeIntakeRecord,
|
|
21
|
+
} from '../intake/index.js';
|
|
22
|
+
|
|
23
|
+
const OPTION_SPEC = {
|
|
24
|
+
artefact: { type: 'string', multiple: true },
|
|
25
|
+
kind: { type: 'string' },
|
|
26
|
+
input: { type: 'string' },
|
|
27
|
+
'dry-run': { type: 'boolean' },
|
|
28
|
+
json: { type: 'boolean' },
|
|
29
|
+
quiet: { type: 'boolean' },
|
|
30
|
+
help: { type: 'boolean' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const HELP = `Usage: rcf intake [--artefact <path>[,path]] [--kind <kind>] [--dry-run]
|
|
34
|
+
rcf intake --input <config.json> [--dry-run]
|
|
35
|
+
|
|
36
|
+
Variable-fidelity intake: read what the operator supplied, classify its
|
|
37
|
+
fidelity, validate it against elicitation integrity, and record an
|
|
38
|
+
intakeClassification block on the manifest. Runs BEFORE the standard
|
|
39
|
+
elicitation playbook when the operator has any starting material.
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
--artefact <path>[,path] One or more artefact file paths (repeatable
|
|
43
|
+
or comma-separated)
|
|
44
|
+
--kind <kind> Hint the artefact kind: napkin |
|
|
45
|
+
productBrief | prd | prdPlusTad | other
|
|
46
|
+
--input <config.json> Non-interactive mode: pre-filled artefacts
|
|
47
|
+
+ findings + acknowledgements
|
|
48
|
+
--dry-run Print the plan without writing the record
|
|
49
|
+
--json Emit the intakeClassification block as JSON
|
|
50
|
+
--quiet Suppress non-error confirmations
|
|
51
|
+
--help Print this help
|
|
52
|
+
|
|
53
|
+
Exit codes:
|
|
54
|
+
0 success
|
|
55
|
+
2 usage error (missing --artefact/--input, unreadable file)
|
|
56
|
+
3 validation failure on the written manifest
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string[]} argv
|
|
61
|
+
* @param {object} [deps]
|
|
62
|
+
* @returns {Promise<number>}
|
|
63
|
+
*/
|
|
64
|
+
export async function main(argv, deps = {}) {
|
|
65
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
66
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
67
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
68
|
+
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
74
|
+
stderr.write(HELP);
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
const flags = parsed.values;
|
|
78
|
+
if (flags.help) { stdout.write(HELP); return 0; }
|
|
79
|
+
|
|
80
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
81
|
+
if (!projectRoot) {
|
|
82
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let input = null;
|
|
87
|
+
if (flags.input) {
|
|
88
|
+
try {
|
|
89
|
+
input = JSON.parse(await readFile(flags.input, 'utf8'));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
stderr.write(`[error] usage intake: cannot read --input file: ${err.message}\n`);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const artefactPaths = [];
|
|
97
|
+
if (input?.artefacts) {
|
|
98
|
+
for (const a of input.artefacts) {
|
|
99
|
+
if (typeof a?.path === 'string' && a.path.length > 0) artefactPaths.push(a.path);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const raw of flags.artefact ?? []) {
|
|
103
|
+
for (const p of raw.split(',').map((s) => s.trim()).filter(Boolean)) artefactPaths.push(p);
|
|
104
|
+
}
|
|
105
|
+
if (artefactPaths.length === 0 && !input?.fidelity) {
|
|
106
|
+
stderr.write('[error] usage intake: at least one --artefact <path> is required (or an --input config with artefacts or a bare fidelity)\n');
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const walkResult = await walkTree({ projectRoot });
|
|
111
|
+
if (walkResult.errors.length > 0) {
|
|
112
|
+
stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding - writes are validated against the post-write state (run 'rcf validate' for details)\n`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const outcome = await runIntakePhases({
|
|
116
|
+
projectRoot,
|
|
117
|
+
artefactPaths,
|
|
118
|
+
kindHint: flags.kind ?? null,
|
|
119
|
+
input,
|
|
120
|
+
});
|
|
121
|
+
if (outcome && outcome.kind && typeof outcome.message === 'string') {
|
|
122
|
+
stderr.write(`[error] ${outcome.kind} ${outcome.message}\n`);
|
|
123
|
+
return outcome.kind === 'usage' ? 2 : 3;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (flags['dry-run']) {
|
|
127
|
+
if (flags.json) {
|
|
128
|
+
stdout.write(`${JSON.stringify(outcome.record, null, 2)}\n`);
|
|
129
|
+
} else {
|
|
130
|
+
stdout.write(`[dry-run] would write intakeClassification ${outcome.record.id} (fidelity=${outcome.record.fidelity}, findings=${outcome.record.validationFindings.length})\n`);
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = await writeIntakeRecord({
|
|
136
|
+
projectRoot,
|
|
137
|
+
tree: walkResult.tree,
|
|
138
|
+
record: outcome.record,
|
|
139
|
+
});
|
|
140
|
+
if (result && result.kind && typeof result.message === 'string') {
|
|
141
|
+
stderr.write(`[error] ${result.kind} ${result.message}\n`);
|
|
142
|
+
return 3;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (flags.json) {
|
|
146
|
+
stdout.write(`${JSON.stringify(outcome.record, null, 2)}\n`);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
if (!flags.quiet) {
|
|
150
|
+
stdout.write(`intake: ${outcome.record.id} fidelity=${outcome.record.fidelity} findings=${outcome.record.validationFindings.length}\n`);
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
package/src/cli/link.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// `rcf link` / `rcf unlink` verb (Phase 4 §D19). Idempotent append/remove
|
|
2
|
+
// of TAC ids on a US's `tacIds[]`. Depends on `@stravica-ai/rcf-schemas`
|
|
3
|
+
// having `US.tacIds[]` defined (0.2.1+).
|
|
4
|
+
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
|
|
7
|
+
import { isRcfError } from '#core/errors';
|
|
8
|
+
import { updateDocument, walkTree } from '#core/store';
|
|
9
|
+
import { findProjectRoot } from '../view/index.js';
|
|
10
|
+
|
|
11
|
+
const OPTION_SPEC = {
|
|
12
|
+
tac: { type: 'string', multiple: true },
|
|
13
|
+
'dry-run': { type: 'boolean' },
|
|
14
|
+
quiet: { type: 'boolean' },
|
|
15
|
+
help: { type: 'boolean' },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const HELP = `Usage: rcf link <us-id> --tac <tac-id> [options]
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--tac <tac-id> TAC id to link (repeatable to link multiple
|
|
22
|
+
TACs in one invocation)
|
|
23
|
+
--dry-run Print the intended write without executing
|
|
24
|
+
--quiet Suppress non-error stdout
|
|
25
|
+
--help Print this help
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export const UNLINK_HELP = `Usage: rcf unlink <us-id> --tac <tac-id> [options]
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--tac <tac-id> TAC id to unlink (repeatable)
|
|
32
|
+
--dry-run Print the intended write without executing
|
|
33
|
+
--quiet Suppress non-error stdout
|
|
34
|
+
--help Print this help
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string[]} argv - argv slice after `link`
|
|
39
|
+
* @param {object} deps
|
|
40
|
+
* @param {boolean} deps.remove - false for link, true for unlink
|
|
41
|
+
* @returns {Promise<number>}
|
|
42
|
+
*/
|
|
43
|
+
export async function main(argv, deps = {}) {
|
|
44
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
45
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
46
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
47
|
+
const removing = Boolean(deps.remove);
|
|
48
|
+
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
54
|
+
stderr.write(help(removing));
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
const flags = parsed.values;
|
|
58
|
+
const positionals = parsed.positionals;
|
|
59
|
+
if (flags.help) { stdout.write(help(removing)); return 0; }
|
|
60
|
+
if (positionals.length !== 1) {
|
|
61
|
+
stderr.write(`[error] usage ${removing ? 'unlink' : 'link'}: expected exactly one <us-id>\n`);
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
const usId = positionals[0];
|
|
65
|
+
const tacIds = flags.tac ?? [];
|
|
66
|
+
if (tacIds.length === 0) {
|
|
67
|
+
stderr.write(`[error] usage ${removing ? 'unlink' : 'link'}: at least one --tac is required\n`);
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
72
|
+
if (!projectRoot) {
|
|
73
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
|
|
74
|
+
return 2;
|
|
75
|
+
}
|
|
76
|
+
const walkResult = await walkTree({ projectRoot });
|
|
77
|
+
// B5: pre-existing tree breakage no longer blocks write verbs - the
|
|
78
|
+
// write is gated on the POST-write tree state inside the writer.
|
|
79
|
+
if (walkResult.errors.length > 0) {
|
|
80
|
+
stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding - writes are validated against the post-write state (run 'rcf validate' for details)\n`);
|
|
81
|
+
}
|
|
82
|
+
const us = walkResult.tree.byId.get(usId);
|
|
83
|
+
if (!us || walkResult.tree.kindById.get(usId) !== 'userStory') {
|
|
84
|
+
stderr.write(`[error] usage ${removing ? 'unlink' : 'link'}: ${usId} is not an existing US\n`);
|
|
85
|
+
return 2;
|
|
86
|
+
}
|
|
87
|
+
for (const tacId of tacIds) {
|
|
88
|
+
if (walkResult.tree.kindById.get(tacId) !== 'tac') {
|
|
89
|
+
stderr.write(`[error] brokenReference ${removing ? 'unlink' : 'link'}: ${tacId} is not an existing TAC\n`);
|
|
90
|
+
return 3;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const current = new Set(us.tacIds ?? []);
|
|
94
|
+
const target = new Set(current);
|
|
95
|
+
for (const tacId of tacIds) {
|
|
96
|
+
if (removing) target.delete(tacId); else target.add(tacId);
|
|
97
|
+
}
|
|
98
|
+
const next = [...target].sort();
|
|
99
|
+
const changed = next.length !== current.size || next.some((id, i) => id !== [...current].sort()[i]);
|
|
100
|
+
if (!changed) {
|
|
101
|
+
if (!flags.quiet) stdout.write(`${usId} tacIds already at target state (idempotent no-op).\n`);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
const result = await updateDocument({
|
|
105
|
+
projectRoot,
|
|
106
|
+
tree: walkResult.tree,
|
|
107
|
+
id: usId,
|
|
108
|
+
patch: next.length === 0 ? { tacIds: [] } : { tacIds: next },
|
|
109
|
+
sets: [],
|
|
110
|
+
options: { dryRun: Boolean(flags['dry-run']) },
|
|
111
|
+
walkErrors: walkResult.errors,
|
|
112
|
+
});
|
|
113
|
+
if (isRcfError(result)) {
|
|
114
|
+
stderr.write(`[error] ${result.kind} ${result.message}\n`);
|
|
115
|
+
if (result.kind === 'validation' || result.kind === 'brokenReference') return 3;
|
|
116
|
+
return 2;
|
|
117
|
+
}
|
|
118
|
+
if (flags['dry-run']) {
|
|
119
|
+
if (!flags.quiet) stdout.write(`[dry-run] would ${removing ? 'unlink' : 'link'} ${usId} tacIds=${JSON.stringify(next)}\n`);
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
if (!flags.quiet) stdout.write(`${usId} tacIds updated (${next.length} entries).\n`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function help(removing) {
|
|
127
|
+
return removing ? UNLINK_HELP : HELP;
|
|
128
|
+
}
|