rcf-lite 0.0.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +344 -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 +71 -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/admissibility/enforce.js +142 -0
- package/src/admissibility/index.js +8 -0
- package/src/admissibility/markers.js +104 -0
- package/src/admissibility/scope-lint.js +163 -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 +358 -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 +168 -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 +221 -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 +214 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +944 -0
- package/src/core/store/writer.js +1879 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +151 -0
- package/src/finalise/index.js +31 -0
- package/src/finalise/ingest.js +160 -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 +1212 -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 +13 -0
- package/src/query/refuse-on-admissibility.js +73 -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/ruleset/index.js +140 -0
- package/src/ruleset/ruleset.json +146 -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 +221 -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 +313 -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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// SSE (Server-Sent Events) connection manager. Tracks the open long-lived
|
|
2
|
+
// `/events` responses, broadcasts payload events to all of them, keeps a
|
|
3
|
+
// heartbeat loop, and drains gracefully on server close (D16).
|
|
4
|
+
//
|
|
5
|
+
// D5: private surface between the server and its own browser client.
|
|
6
|
+
// Neither payload shape nor route path is a stable contract; downstream
|
|
7
|
+
// consumers (e.g. `rcf query`) get their own wire when the time comes.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} SseHubOptions
|
|
11
|
+
* @property {number} [heartbeatMs=30000] - heartbeat interval in ms
|
|
12
|
+
* @property {(msg: string) => void} [log] - optional stderr sink
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {SseHubOptions} [opts]
|
|
17
|
+
* @returns {{
|
|
18
|
+
* handle: (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, current: { version: number, contentHtml: string } | null) => void,
|
|
19
|
+
* broadcast: (event: string, data: object) => void,
|
|
20
|
+
* heartbeatOnce: () => void,
|
|
21
|
+
* drain: (event?: string) => Promise<void>,
|
|
22
|
+
* size: () => number,
|
|
23
|
+
* close: () => void,
|
|
24
|
+
* }}
|
|
25
|
+
*/
|
|
26
|
+
export function createSseHub(opts = {}) {
|
|
27
|
+
const heartbeatMs = typeof opts.heartbeatMs === 'number' && opts.heartbeatMs > 0 ? opts.heartbeatMs : 30000;
|
|
28
|
+
const log = typeof opts.log === 'function' ? opts.log : () => {};
|
|
29
|
+
/** @type {Set<import('node:http').ServerResponse>} */
|
|
30
|
+
const clients = new Set();
|
|
31
|
+
let heartbeatTimer = null;
|
|
32
|
+
let closed = false;
|
|
33
|
+
|
|
34
|
+
function frame(event, data) {
|
|
35
|
+
const payload = typeof data === 'string' ? data : JSON.stringify(data ?? {});
|
|
36
|
+
return `event: ${event}\ndata: ${payload}\n\n`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeTo(res, event, data) {
|
|
40
|
+
try {
|
|
41
|
+
res.write(frame(event, data));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
log(`[sse] write failed: ${/** @type {Error} */ (err).message}`);
|
|
44
|
+
try { res.end(); } catch { /* swallow */ }
|
|
45
|
+
clients.delete(res);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function startHeartbeat() {
|
|
50
|
+
if (heartbeatTimer || closed) return;
|
|
51
|
+
heartbeatTimer = setInterval(() => {
|
|
52
|
+
if (clients.size === 0) return;
|
|
53
|
+
const ts = new Date().toISOString();
|
|
54
|
+
for (const res of clients) writeTo(res, 'heartbeat', { ts });
|
|
55
|
+
}, heartbeatMs);
|
|
56
|
+
if (typeof heartbeatTimer.unref === 'function') heartbeatTimer.unref();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stopHeartbeat() {
|
|
60
|
+
if (!heartbeatTimer) return;
|
|
61
|
+
clearInterval(heartbeatTimer);
|
|
62
|
+
heartbeatTimer = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
handle(req, res, current) {
|
|
67
|
+
if (closed) {
|
|
68
|
+
res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
|
|
69
|
+
res.end('server shutting down\n');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
res.writeHead(200, {
|
|
73
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
74
|
+
'cache-control': 'no-cache, no-transform',
|
|
75
|
+
'connection': 'keep-alive',
|
|
76
|
+
'x-accel-buffering': 'no',
|
|
77
|
+
});
|
|
78
|
+
// Prime the stream so proxies don't buffer the initial event.
|
|
79
|
+
try { res.write(': ok\n\n'); } catch { /* swallow */ }
|
|
80
|
+
clients.add(res);
|
|
81
|
+
req.on('close', () => {
|
|
82
|
+
clients.delete(res);
|
|
83
|
+
});
|
|
84
|
+
// First event on connect carries the current state so the client
|
|
85
|
+
// is in sync without a separate resume protocol (D12).
|
|
86
|
+
if (current) writeTo(res, 'tree-update', current);
|
|
87
|
+
startHeartbeat();
|
|
88
|
+
},
|
|
89
|
+
broadcast(event, data) {
|
|
90
|
+
if (closed) return;
|
|
91
|
+
for (const res of clients) writeTo(res, event, data);
|
|
92
|
+
},
|
|
93
|
+
heartbeatOnce() {
|
|
94
|
+
if (clients.size === 0) return;
|
|
95
|
+
const ts = new Date().toISOString();
|
|
96
|
+
for (const res of clients) writeTo(res, 'heartbeat', { ts });
|
|
97
|
+
},
|
|
98
|
+
async drain(event = 'shutdown') {
|
|
99
|
+
if (closed) return;
|
|
100
|
+
closed = true;
|
|
101
|
+
stopHeartbeat();
|
|
102
|
+
for (const res of clients) {
|
|
103
|
+
writeTo(res, event, {});
|
|
104
|
+
try { res.end(); } catch { /* swallow */ }
|
|
105
|
+
}
|
|
106
|
+
clients.clear();
|
|
107
|
+
},
|
|
108
|
+
size() { return clients.size; },
|
|
109
|
+
close() {
|
|
110
|
+
closed = true;
|
|
111
|
+
stopHeartbeat();
|
|
112
|
+
for (const res of clients) {
|
|
113
|
+
try { res.end(); } catch { /* swallow */ }
|
|
114
|
+
}
|
|
115
|
+
clients.clear();
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// Pre-session agent bootstrap (Theme 1, E2E matrix 2026-07-06-003;
|
|
2
|
+
// updated for 0.6.0 init-hygiene). `rcf init` is the single golden path
|
|
3
|
+
// that leaves a project fully wired BEFORE the agent session starts:
|
|
4
|
+
// rcf/ tree + project-root .mcp.json (rcf server entry) + the managed
|
|
5
|
+
// canonical block inside marker comments in the agent-instructions
|
|
6
|
+
// file. Anything that detects incomplete setup funnels back here: run
|
|
7
|
+
// `npx rcf init`, then restart the agent session.
|
|
8
|
+
//
|
|
9
|
+
// 0.6.0 changes:
|
|
10
|
+
// - Marker constants moved to `managed-markers.js` and imported here
|
|
11
|
+
// (D-7); the strand-1 legacy migration recognises both generations.
|
|
12
|
+
// - Canonical block content is sourced from
|
|
13
|
+
// `guidance/managed/agent-instructions-block.md`, not from the
|
|
14
|
+
// `guidance/harness-template.md` fenced fragment. The two remain
|
|
15
|
+
// byte-identical after `scripts/gen-managed-artefacts.mjs` runs
|
|
16
|
+
// (AC-1.14); harness-template.md stays as the manual-paste-in doc.
|
|
17
|
+
// - `hasAgentMarker` recognises BOTH the new managed markers and the
|
|
18
|
+
// pre-0.6.0 legacy markers so the MCP setup funnel does not spam
|
|
19
|
+
// legacy-inited repos with "Setup incomplete" notices (§2.5 / AC-1.13).
|
|
20
|
+
|
|
21
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
22
|
+
import { dirname, join, resolve } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
import { rcfError } from '#core/errors';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
MARKER_BEGIN,
|
|
29
|
+
MARKER_END,
|
|
30
|
+
LEGACY_MARKER_BEGIN,
|
|
31
|
+
markerRegex,
|
|
32
|
+
} from './managed-markers.js';
|
|
33
|
+
|
|
34
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const PACKAGE_ROOT = resolve(here, '..', '..');
|
|
36
|
+
const MANAGED_BLOCK_PATH = join(PACKAGE_ROOT, 'guidance', 'managed', 'agent-instructions-block.md');
|
|
37
|
+
const MANAGED_HASH_PATH = join(PACKAGE_ROOT, 'guidance', 'managed', 'agent-instructions-block.hash');
|
|
38
|
+
const LEGACY_FRAGMENT_HASHES_PATH = join(PACKAGE_ROOT, 'guidance', 'managed', 'legacy-fragment-hashes.json');
|
|
39
|
+
|
|
40
|
+
// Re-export the marker constants at the module boundary so callers that
|
|
41
|
+
// already import from `agent-setup.js` (setup funnel, existing tests)
|
|
42
|
+
// keep working without touching every import site.
|
|
43
|
+
export { MARKER_BEGIN, MARKER_END, LEGACY_MARKER_BEGIN } from './managed-markers.js';
|
|
44
|
+
|
|
45
|
+
/** Absolute path of this package's rcf bin - what .mcp.json points at. */
|
|
46
|
+
export function rcfBinPath() {
|
|
47
|
+
return join(PACKAGE_ROOT, 'bin', 'rcf.js');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Absolute path of the canonical managed block; test-visible for the byte-match AC. */
|
|
51
|
+
export function managedBlockPath() {
|
|
52
|
+
return MANAGED_BLOCK_PATH;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Absolute path of the canonical managed-block hash file. */
|
|
56
|
+
export function managedBlockHashPath() {
|
|
57
|
+
return MANAGED_HASH_PATH;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Absolute path of the pre-0.6.0 canonical-fragment hash whitelist that
|
|
62
|
+
* §7.3's fail-safe hand-edit detector consults. Test-visible.
|
|
63
|
+
*/
|
|
64
|
+
export function legacyFragmentHashesPath() {
|
|
65
|
+
return LEGACY_FRAGMENT_HASHES_PATH;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fileExists(path) {
|
|
69
|
+
try {
|
|
70
|
+
await readFile(path, 'utf8');
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function readIfExists(path) {
|
|
78
|
+
try {
|
|
79
|
+
return await readFile(path, 'utf8');
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return null;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Read the canonical managed block text (§2.4 verbatim). Returns the
|
|
88
|
+
* text with a trailing newline; callers wrap it in the markers. Fails
|
|
89
|
+
* with an RcfError if the shipped asset is missing (bad tarball, files
|
|
90
|
+
* whitelist regression) - doctor surfaces this rather than pretending
|
|
91
|
+
* clean (§12 risk).
|
|
92
|
+
*
|
|
93
|
+
* @returns {Promise<string | import('#core/errors').RcfError>}
|
|
94
|
+
*/
|
|
95
|
+
export async function loadManagedBlock() {
|
|
96
|
+
const text = await readIfExists(MANAGED_BLOCK_PATH);
|
|
97
|
+
if (text === null) {
|
|
98
|
+
return rcfError({ kind: 'missingFile', message: `managed block not found: ${MANAGED_BLOCK_PATH}`, filePath: MANAGED_BLOCK_PATH });
|
|
99
|
+
}
|
|
100
|
+
return text.endsWith('\n') ? text : `${text}\n`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Read the SHA-256 hash the package shipped for the managed block. One
|
|
105
|
+
* line, no trailing whitespace; on read failure, returns an RcfError
|
|
106
|
+
* with kind `hashFileMissing` so doctor emits a distinct error rather
|
|
107
|
+
* than reporting spurious clean.
|
|
108
|
+
*
|
|
109
|
+
* @returns {Promise<string | import('#core/errors').RcfError>}
|
|
110
|
+
*/
|
|
111
|
+
export async function loadManagedBlockHash() {
|
|
112
|
+
const text = await readIfExists(MANAGED_HASH_PATH);
|
|
113
|
+
if (text === null) {
|
|
114
|
+
return rcfError({ kind: 'hashFileMissing', message: `managed-block hash not found: ${MANAGED_HASH_PATH}`, filePath: MANAGED_HASH_PATH });
|
|
115
|
+
}
|
|
116
|
+
return text.trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read the SHA-256 whitelist of pre-0.6.0 canonical fragments (§7.3).
|
|
121
|
+
* Doctor's `detectLegacyHandEdits` hashes the extracted legacy inner
|
|
122
|
+
* content (trimmed) and treats any hash NOT in the returned set as
|
|
123
|
+
* hand-edited. Fail-safe: a missing / malformed whitelist surfaces as
|
|
124
|
+
* an RcfError rather than silently allowing overwrite.
|
|
125
|
+
*
|
|
126
|
+
* @returns {Promise<Set<string> | import('#core/errors').RcfError>}
|
|
127
|
+
*/
|
|
128
|
+
export async function loadLegacyFragmentHashes() {
|
|
129
|
+
const text = await readIfExists(LEGACY_FRAGMENT_HASHES_PATH);
|
|
130
|
+
if (text === null) {
|
|
131
|
+
return rcfError({
|
|
132
|
+
kind: 'legacyFragmentHashesMissing',
|
|
133
|
+
message: `legacy fragment hash whitelist not found: ${LEGACY_FRAGMENT_HASHES_PATH}`,
|
|
134
|
+
filePath: LEGACY_FRAGMENT_HASHES_PATH,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(text);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return rcfError({
|
|
142
|
+
kind: 'legacyFragmentHashesInvalid',
|
|
143
|
+
message: `legacy fragment hash whitelist is not valid JSON: ${err.message}`,
|
|
144
|
+
filePath: LEGACY_FRAGMENT_HASHES_PATH,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.hashes)) {
|
|
148
|
+
return rcfError({
|
|
149
|
+
kind: 'legacyFragmentHashesInvalid',
|
|
150
|
+
message: `legacy fragment hash whitelist missing hashes[] array`,
|
|
151
|
+
filePath: LEGACY_FRAGMENT_HASHES_PATH,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const set = new Set();
|
|
155
|
+
for (const entry of parsed.hashes) {
|
|
156
|
+
if (entry && typeof entry.hash === 'string' && /^[0-9a-f]{64}$/i.test(entry.hash)) {
|
|
157
|
+
set.add(entry.hash.toLowerCase());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return set;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extract the paste-in fragment from guidance/harness-template.md (the
|
|
165
|
+
* first ```markdown fence). Preserved for the paste-in doc use case,
|
|
166
|
+
* but the fragment is regenerated at package-build time from the
|
|
167
|
+
* managed block canonical source (AC-1.14) so the two are byte-identical.
|
|
168
|
+
*
|
|
169
|
+
* @param {object} [opts]
|
|
170
|
+
* @param {string} [opts.templatePath] - test override
|
|
171
|
+
* @returns {Promise<string | import('#core/errors').RcfError>}
|
|
172
|
+
*/
|
|
173
|
+
export async function loadHarnessFragment({ templatePath } = {}) {
|
|
174
|
+
const path = templatePath ?? join(PACKAGE_ROOT, 'guidance', 'harness-template.md');
|
|
175
|
+
const text = await readIfExists(path);
|
|
176
|
+
if (text === null) {
|
|
177
|
+
return rcfError({ kind: 'missingFile', message: `harness template not found: ${path}`, filePath: path });
|
|
178
|
+
}
|
|
179
|
+
const m = /```markdown\n([\s\S]*?)```/.exec(text);
|
|
180
|
+
if (!m) {
|
|
181
|
+
return rcfError({ kind: 'parseFailure', message: `no \`\`\`markdown fragment fence in ${path}`, filePath: path });
|
|
182
|
+
}
|
|
183
|
+
return m[1].trim();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Write or merge the project-root .mcp.json with the rcf server entry
|
|
188
|
+
* (the exact registration shape docs/install.md documents). MERGE
|
|
189
|
+
* discipline: other servers and unknown top-level keys are preserved
|
|
190
|
+
* verbatim; an existing `rcf` entry is left alone.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} args
|
|
193
|
+
* @param {string} args.projectRoot
|
|
194
|
+
* @param {string} [args.binPath] - test override
|
|
195
|
+
* @returns {Promise<{ file: string, action: 'created'|'merged'|'kept' } | import('#core/errors').RcfError>}
|
|
196
|
+
*/
|
|
197
|
+
export async function writeMcpConfig({ projectRoot, binPath = rcfBinPath() }) {
|
|
198
|
+
const file = join(projectRoot, '.mcp.json');
|
|
199
|
+
const raw = await readIfExists(file);
|
|
200
|
+
let config = {};
|
|
201
|
+
let action = 'created';
|
|
202
|
+
if (raw !== null) {
|
|
203
|
+
try {
|
|
204
|
+
config = JSON.parse(raw);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return rcfError({
|
|
207
|
+
kind: 'parseFailure',
|
|
208
|
+
message: `.mcp.json exists but is not valid JSON (${err.message}); refusing to modify it. Fix it by hand, or add the rcf entry manually - see docs/install.md, section 7.`,
|
|
209
|
+
filePath: '.mcp.json',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
213
|
+
return rcfError({
|
|
214
|
+
kind: 'parseFailure',
|
|
215
|
+
message: '.mcp.json exists but is not a JSON object; refusing to modify it.',
|
|
216
|
+
filePath: '.mcp.json',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
action = 'merged';
|
|
220
|
+
}
|
|
221
|
+
const servers = (config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers))
|
|
222
|
+
? config.mcpServers
|
|
223
|
+
: {};
|
|
224
|
+
if (servers.rcf) {
|
|
225
|
+
return { file: '.mcp.json', action: 'kept' };
|
|
226
|
+
}
|
|
227
|
+
const next = {
|
|
228
|
+
...config,
|
|
229
|
+
mcpServers: {
|
|
230
|
+
...servers,
|
|
231
|
+
rcf: { command: 'node', args: [binPath, 'mcp'] },
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
await writeFile(file, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
235
|
+
return { file: '.mcp.json', action };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Write the composed managed block into one agent-instructions file.
|
|
240
|
+
* Idempotent: an existing new-marker block is replaced in place, never
|
|
241
|
+
* duplicated; a file without one gets the block appended; a missing
|
|
242
|
+
* file is created. Init does not touch legacy markers - that migration
|
|
243
|
+
* is doctor's `--fix` path, so an existing pre-0.6.0 file whose only
|
|
244
|
+
* marker pair is the legacy one gets a NEW managed block appended
|
|
245
|
+
* alongside the legacy one, and the operator resolves via `rcf doctor
|
|
246
|
+
* --fix` afterwards. This preserves init's "leave operator content
|
|
247
|
+
* alone" discipline even for a file that also happens to carry the old
|
|
248
|
+
* managed convention.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} target - absolute path
|
|
251
|
+
* @param {string} file - display name (CLAUDE.md / AGENTS.md)
|
|
252
|
+
* @param {string} block - the marked fragment block
|
|
253
|
+
* @returns {Promise<{ file: string, action: 'created'|'appended'|'replaced' }>}
|
|
254
|
+
*/
|
|
255
|
+
async function writeFragmentToFile(target, file, block) {
|
|
256
|
+
const existing = await readIfExists(target);
|
|
257
|
+
if (existing === null) {
|
|
258
|
+
await writeFile(target, `${block}\n`, 'utf8');
|
|
259
|
+
return { file, action: 'created' };
|
|
260
|
+
}
|
|
261
|
+
const markerRe = markerRegex();
|
|
262
|
+
if (markerRe.test(existing)) {
|
|
263
|
+
await writeFile(target, existing.replace(markerRe, block), 'utf8');
|
|
264
|
+
return { file, action: 'replaced' };
|
|
265
|
+
}
|
|
266
|
+
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
267
|
+
await writeFile(target, `${existing}${sep}${block}\n`, 'utf8');
|
|
268
|
+
return { file, action: 'appended' };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Write the canonical managed block into the project's agent-instructions
|
|
273
|
+
* file(s) inside the rcf managed markers. Routing:
|
|
274
|
+
* - An existing instructions file is refreshed in place (CLAUDE.md
|
|
275
|
+
* preferred as the write target, else an existing AGENTS.md). We
|
|
276
|
+
* never invent the other convention's file when one already exists.
|
|
277
|
+
* - A fresh repo (neither present) gets BOTH CLAUDE.md and AGENTS.md,
|
|
278
|
+
* so the wiring is vendor-neutral by default (operator ruling
|
|
279
|
+
* 2026-07-16). The same marked block goes into each.
|
|
280
|
+
* Idempotent throughout: re-running replaces the marked block in place,
|
|
281
|
+
* never duplicating it, in whichever file(s) are touched.
|
|
282
|
+
*
|
|
283
|
+
* @param {object} args
|
|
284
|
+
* @param {string} args.projectRoot
|
|
285
|
+
* @param {string} args.fragment - canonical block inner content (no markers).
|
|
286
|
+
* @returns {Promise<{ writes: Array<{ file: string, action: 'created'|'appended'|'replaced' }> }>}
|
|
287
|
+
*/
|
|
288
|
+
export async function writeAgentInstructions({ projectRoot, fragment }) {
|
|
289
|
+
const claudePath = join(projectRoot, 'CLAUDE.md');
|
|
290
|
+
const agentsPath = join(projectRoot, 'AGENTS.md');
|
|
291
|
+
const claudeExists = await fileExists(claudePath);
|
|
292
|
+
const agentsExists = await fileExists(agentsPath);
|
|
293
|
+
const trimmed = fragment.trim();
|
|
294
|
+
const block = `${MARKER_BEGIN}\n${trimmed}\n${MARKER_END}`;
|
|
295
|
+
const writes = [];
|
|
296
|
+
|
|
297
|
+
if (claudeExists) {
|
|
298
|
+
// Existing CLAUDE.md wins as the target; refresh it in place.
|
|
299
|
+
writes.push(await writeFragmentToFile(claudePath, 'CLAUDE.md', block));
|
|
300
|
+
} else if (agentsExists) {
|
|
301
|
+
// No CLAUDE.md, but an AGENTS.md is present: keep that routing.
|
|
302
|
+
writes.push(await writeFragmentToFile(agentsPath, 'AGENTS.md', block));
|
|
303
|
+
} else {
|
|
304
|
+
// Fresh repo: write both, vendor-neutral by default.
|
|
305
|
+
writes.push(await writeFragmentToFile(claudePath, 'CLAUDE.md', block));
|
|
306
|
+
writes.push(await writeFragmentToFile(agentsPath, 'AGENTS.md', block));
|
|
307
|
+
}
|
|
308
|
+
return { writes };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Does the project's agent-instructions file carry ANY generation of
|
|
313
|
+
* the rcf marker block? The MCP setup funnel uses this: marker absent
|
|
314
|
+
* means the session started without the init bootstrap.
|
|
315
|
+
*
|
|
316
|
+
* Recognises BOTH the 0.6.0+ managed marker (MARKER_BEGIN) and the
|
|
317
|
+
* pre-0.6.0 legacy marker (LEGACY_MARKER_BEGIN). Either presence
|
|
318
|
+
* satisfies the funnel so a legacy-inited repo is not spammed with
|
|
319
|
+
* setup notices for the process lifetime while it waits to migrate;
|
|
320
|
+
* the correct signal for "you should migrate" is doctor's
|
|
321
|
+
* `legacy-markers` drift item, which the operator sees on their next
|
|
322
|
+
* diagnostic run. §2.5, AC-1.13.
|
|
323
|
+
*
|
|
324
|
+
* @param {string} projectRoot
|
|
325
|
+
* @returns {Promise<boolean>}
|
|
326
|
+
*/
|
|
327
|
+
export async function hasAgentMarker(projectRoot) {
|
|
328
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
329
|
+
const text = await readIfExists(join(projectRoot, name));
|
|
330
|
+
if (text === null) continue;
|
|
331
|
+
if (text.includes(MARKER_BEGIN)) return true;
|
|
332
|
+
if (text.includes(LEGACY_MARKER_BEGIN)) return true;
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The single funnel instruction every incomplete-setup touchpoint
|
|
339
|
+
* repeats. One golden path; no degraded mid-session fallback.
|
|
340
|
+
*/
|
|
341
|
+
export const SETUP_FUNNEL_INSTRUCTION = 'Setup incomplete. Run `npx rcf init` to finish wiring '
|
|
342
|
+
+ '(tree + .mcp.json + agent instructions), then tell the user to exit and restart their '
|
|
343
|
+
+ 'agent session before continuing.';
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Manual instructions printed by `rcf init --no-agent-setup`.
|
|
347
|
+
*
|
|
348
|
+
* @param {string} [binPath]
|
|
349
|
+
* @returns {string}
|
|
350
|
+
*/
|
|
351
|
+
export function manualSetupInstructions(binPath = rcfBinPath()) {
|
|
352
|
+
return [
|
|
353
|
+
'Agent setup skipped (--no-agent-setup). To wire the harness manually:',
|
|
354
|
+
' 1. Register the MCP server in your project-root .mcp.json:',
|
|
355
|
+
' { "mcpServers": { "rcf": { "command": "node",',
|
|
356
|
+
` "args": ["${binPath}", "mcp"] } } }`,
|
|
357
|
+
' 2. Paste the fragment from `rcf guidance harness-template` (the',
|
|
358
|
+
' first ```markdown fence) into your project\'s CLAUDE.md or',
|
|
359
|
+
' AGENTS.md.',
|
|
360
|
+
' 3. Restart your agent session so it picks both up.',
|
|
361
|
+
].join('\n');
|
|
362
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Strand 3: seed writer for `rcf/.identity/profile.md` and the entry
|
|
2
|
+
// constant the managed-gitignore aggregator consumes to keep the
|
|
3
|
+
// directory ignored by default (0.6.0 spec §5, §4.1).
|
|
4
|
+
//
|
|
5
|
+
// Two exports:
|
|
6
|
+
// - `identityEntry`: the aggregator's registered entry; imported by
|
|
7
|
+
// managed-gitignore.js and inserted into `managedGitignoreEntries()`.
|
|
8
|
+
// - `writeIdentityTemplate`: init calls this after the tree scaffold.
|
|
9
|
+
// Idempotent: an existing profile.md is left byte-identical (§5.4).
|
|
10
|
+
//
|
|
11
|
+
// The template is a one-shot seed; doctor does not maintain its content
|
|
12
|
+
// (§5.5 — the profile is entirely operator-owned once written). Doctor
|
|
13
|
+
// cares only about presence and effective-ignore, both warn-only.
|
|
14
|
+
|
|
15
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Entry constant consumed by `managed-gitignore.js`'s aggregator. Adding
|
|
20
|
+
* this to `managedGitignoreEntries()` keeps `rcf/.identity/` out of the
|
|
21
|
+
* shared repo by default (§4.1, §4.3, §5.1). The 0.7.0 credentials
|
|
22
|
+
* side-file will follow the same pattern: import an entry constant from
|
|
23
|
+
* its owning module, insert into the array, done.
|
|
24
|
+
*/
|
|
25
|
+
export const identityEntry = Object.freeze({
|
|
26
|
+
path: 'rcf/.identity/',
|
|
27
|
+
owner: 'rcf init: per-clone operator profile',
|
|
28
|
+
since: '0.6.0',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Canonical template written verbatim on fresh init (spec §5.4). British
|
|
33
|
+
* English, no em-dashes, banned-tells baseline honoured; the same lint
|
|
34
|
+
* fires against this constant per AC-4.7.
|
|
35
|
+
*/
|
|
36
|
+
export const IDENTITY_TEMPLATE = `# Operator profile
|
|
37
|
+
|
|
38
|
+
This file describes you (the operator) to any agent working on this
|
|
39
|
+
repo. It lives in \`rcf/.identity/\` and is gitignored by default, so
|
|
40
|
+
it is per-clone: another developer's clone of the same repo has their
|
|
41
|
+
own profile, not yours.
|
|
42
|
+
|
|
43
|
+
Fill in what is useful. Leave the rest. Freeform prose is fine; a
|
|
44
|
+
bulleted list is fine. Nothing here is enforced by tooling.
|
|
45
|
+
|
|
46
|
+
## Name
|
|
47
|
+
|
|
48
|
+
_(who you are; how you want the agent to address you)_
|
|
49
|
+
|
|
50
|
+
## Role
|
|
51
|
+
|
|
52
|
+
_(what you do; what perspective you bring to this project)_
|
|
53
|
+
|
|
54
|
+
## Working preferences
|
|
55
|
+
|
|
56
|
+
_(how you like to work: casual or formal register, verbose or terse
|
|
57
|
+
responses, willingness to be pushed back on, anything the agent should
|
|
58
|
+
know before choosing its default posture)_
|
|
59
|
+
|
|
60
|
+
## Project-scoped notes
|
|
61
|
+
|
|
62
|
+
_(anything specific to this project that would be useful for the agent
|
|
63
|
+
to know but does not belong in the shared requirements tree: a local
|
|
64
|
+
dev workflow quirk, an in-flight side-experiment, a "do not touch this
|
|
65
|
+
directory yet" flag)_
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
If you want to share your profile with the team, remove the
|
|
70
|
+
\`rcf/.identity/\` line from the managed block in \`.gitignore\` (or \`git
|
|
71
|
+
add -f rcf/.identity/profile.md\` for one-off sharing). The default is
|
|
72
|
+
per-clone. Sharing is deliberate.
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Absolute path of the profile file for a given project root.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectRoot
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
export function identityProfilePath(projectRoot) {
|
|
82
|
+
return join(projectRoot, 'rcf', '.identity', 'profile.md');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Seed the identity template if not already present. Idempotent - an
|
|
87
|
+
* existing file (any content) is left byte-identical, per AC-4.2.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} args
|
|
90
|
+
* @param {string} args.projectRoot
|
|
91
|
+
* @returns {Promise<{ file: string, action: 'created' | 'kept' }>}
|
|
92
|
+
*/
|
|
93
|
+
export async function writeIdentityTemplate({ projectRoot }) {
|
|
94
|
+
const path = identityProfilePath(projectRoot);
|
|
95
|
+
try {
|
|
96
|
+
await readFile(path, 'utf8');
|
|
97
|
+
return { file: 'rcf/.identity/profile.md', action: 'kept' };
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code !== 'ENOENT') throw err;
|
|
100
|
+
}
|
|
101
|
+
await mkdir(dirname(path), { recursive: true });
|
|
102
|
+
await writeFile(path, IDENTITY_TEMPLATE, 'utf8');
|
|
103
|
+
return { file: 'rcf/.identity/profile.md', action: 'created' };
|
|
104
|
+
}
|