ai-collab-open-system 0.1.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/.aict/START_HERE.md +127 -0
- package/.aict/WORKSPACE_MANIFEST.json +91 -0
- package/.aict/acceptance/EXAMPLE.synthetic.md +49 -0
- package/.aict/acceptance/FAILURE_MODES.md +40 -0
- package/.aict/acceptance/PROMPT.md +47 -0
- package/.aict/acceptance/README.md +44 -0
- package/.aict/acceptance/TEMPLATE.md +57 -0
- package/.aict/adapters/SHARED_CORE_CONTRACT.md +106 -0
- package/.aict/adapters/claude-code/ADAPTER.md +28 -0
- package/.aict/adapters/cline/ADAPTER.md +28 -0
- package/.aict/adapters/codex/ADAPTER.md +28 -0
- package/.aict/adapters/copilot/ADAPTER.md +28 -0
- package/.aict/adapters/cursor/ADAPTER.md +28 -0
- package/.aict/adapters/windsurf/ADAPTER.md +28 -0
- package/.aict/context/EXAMPLE.synthetic.md +53 -0
- package/.aict/context/FAILURE_MODES.md +40 -0
- package/.aict/context/PROMPT.md +47 -0
- package/.aict/context/README.md +44 -0
- package/.aict/context/TEMPLATE.md +63 -0
- package/.aict/cookbook/README.md +8 -0
- package/.aict/cookbook/bridge-to-a-second-family.md +103 -0
- package/.aict/cookbook/connect-a-tool.md +67 -0
- package/.aict/cookbook/review-a-half-product.md +79 -0
- package/.aict/cookbook/run-a-first-loop.md +81 -0
- package/.aict/examples/README.md +21 -0
- package/.aict/examples/ai-coding-long-task/CASE.md +161 -0
- package/.aict/examples/ai-coding-long-task/artifacts/acceptance-card.md +36 -0
- package/.aict/examples/ai-coding-long-task/artifacts/context-package.md +30 -0
- package/.aict/examples/ai-coding-long-task/artifacts/execution-prompt.md +30 -0
- package/.aict/examples/ai-coding-long-task/artifacts/first-ai-output.md +109 -0
- package/.aict/examples/ai-coding-long-task/artifacts/guard-review.md +40 -0
- package/.aict/examples/ai-coding-long-task/artifacts/handoff-note.md +28 -0
- package/.aict/examples/ai-coding-long-task/artifacts/harvest-seed.md +28 -0
- package/.aict/examples/ai-coding-long-task/artifacts/revised-output.md +62 -0
- package/.aict/examples/content-production-harvest/CASE.md +87 -0
- package/.aict/examples/content-production-harvest/artifacts/acceptance-card.md +28 -0
- package/.aict/examples/content-production-harvest/artifacts/context-package.md +28 -0
- package/.aict/examples/content-production-harvest/artifacts/execution-prompt.md +30 -0
- package/.aict/examples/content-production-harvest/artifacts/guard-review.md +28 -0
- package/.aict/examples/content-production-harvest/artifacts/handoff-note.md +28 -0
- package/.aict/examples/content-production-harvest/artifacts/harvest-seed.md +28 -0
- package/.aict/examples/multi-tool-collaboration/CASE.md +87 -0
- package/.aict/examples/multi-tool-collaboration/artifacts/acceptance-card.md +28 -0
- package/.aict/examples/multi-tool-collaboration/artifacts/context-package.md +28 -0
- package/.aict/examples/multi-tool-collaboration/artifacts/execution-prompt.md +30 -0
- package/.aict/examples/multi-tool-collaboration/artifacts/guard-review.md +28 -0
- package/.aict/examples/multi-tool-collaboration/artifacts/handoff-note.md +28 -0
- package/.aict/examples/multi-tool-collaboration/artifacts/harvest-seed.md +28 -0
- package/.aict/examples/personal-judgment-growth-assistant/CASE.md +87 -0
- package/.aict/examples/personal-judgment-growth-assistant/artifacts/acceptance-card.md +28 -0
- package/.aict/examples/personal-judgment-growth-assistant/artifacts/context-package.md +28 -0
- package/.aict/examples/personal-judgment-growth-assistant/artifacts/execution-prompt.md +30 -0
- package/.aict/examples/personal-judgment-growth-assistant/artifacts/guard-review.md +28 -0
- package/.aict/examples/personal-judgment-growth-assistant/artifacts/handoff-note.md +28 -0
- package/.aict/examples/personal-judgment-growth-assistant/artifacts/harvest-seed.md +28 -0
- package/.aict/examples/research-knowledge-synthesis/CASE.md +87 -0
- package/.aict/examples/research-knowledge-synthesis/artifacts/acceptance-card.md +28 -0
- package/.aict/examples/research-knowledge-synthesis/artifacts/context-package.md +28 -0
- package/.aict/examples/research-knowledge-synthesis/artifacts/execution-prompt.md +30 -0
- package/.aict/examples/research-knowledge-synthesis/artifacts/guard-review.md +28 -0
- package/.aict/examples/research-knowledge-synthesis/artifacts/handoff-note.md +28 -0
- package/.aict/examples/research-knowledge-synthesis/artifacts/harvest-seed.md +28 -0
- package/.aict/guard/EXAMPLE.synthetic.md +51 -0
- package/.aict/guard/FAILURE_MODES.md +40 -0
- package/.aict/guard/PROMPT.md +47 -0
- package/.aict/guard/README.md +44 -0
- package/.aict/guard/TEMPLATE.md +60 -0
- package/.aict/handoff/EXAMPLE.synthetic.md +51 -0
- package/.aict/handoff/FAILURE_MODES.md +40 -0
- package/.aict/handoff/PROMPT.md +47 -0
- package/.aict/handoff/README.md +44 -0
- package/.aict/handoff/TEMPLATE.md +60 -0
- package/.aict/harvest/EXAMPLE.synthetic.md +51 -0
- package/.aict/harvest/FAILURE_MODES.md +40 -0
- package/.aict/harvest/PROMPT.md +47 -0
- package/.aict/harvest/README.md +44 -0
- package/.aict/harvest/TEMPLATE.md +60 -0
- package/.aict/mechanisms/README.md +34 -0
- package/.aict/mechanisms/anti-drift-partner/EXAMPLE.synthetic.md +46 -0
- package/.aict/mechanisms/anti-drift-partner/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/anti-drift-partner/PROMPT.md +75 -0
- package/.aict/mechanisms/anti-drift-partner/README.md +82 -0
- package/.aict/mechanisms/anti-drift-partner/TEMPLATE.md +74 -0
- package/.aict/mechanisms/blind-spot-scan/EXAMPLE.synthetic.md +39 -0
- package/.aict/mechanisms/blind-spot-scan/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/blind-spot-scan/PROMPT.md +72 -0
- package/.aict/mechanisms/blind-spot-scan/README.md +79 -0
- package/.aict/mechanisms/blind-spot-scan/TEMPLATE.md +70 -0
- package/.aict/mechanisms/collaboration-coach/EXAMPLE.synthetic.md +40 -0
- package/.aict/mechanisms/collaboration-coach/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/collaboration-coach/PROMPT.md +72 -0
- package/.aict/mechanisms/collaboration-coach/README.md +79 -0
- package/.aict/mechanisms/collaboration-coach/TEMPLATE.md +61 -0
- package/.aict/mechanisms/do-not-handle-yet/EXAMPLE.synthetic.md +15 -0
- package/.aict/mechanisms/do-not-handle-yet/FAILURE_MODES.md +16 -0
- package/.aict/mechanisms/do-not-handle-yet/PROMPT.md +41 -0
- package/.aict/mechanisms/do-not-handle-yet/README.md +30 -0
- package/.aict/mechanisms/do-not-handle-yet/TEMPLATE.md +38 -0
- package/.aict/mechanisms/dual-guard/EXAMPLE.synthetic.md +54 -0
- package/.aict/mechanisms/dual-guard/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/dual-guard/PROMPT.md +76 -0
- package/.aict/mechanisms/dual-guard/README.md +81 -0
- package/.aict/mechanisms/dual-guard/TEMPLATE.md +73 -0
- package/.aict/mechanisms/feedback-absorption-ledger/EXAMPLE.synthetic.md +49 -0
- package/.aict/mechanisms/feedback-absorption-ledger/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/feedback-absorption-ledger/PROMPT.md +74 -0
- package/.aict/mechanisms/feedback-absorption-ledger/README.md +81 -0
- package/.aict/mechanisms/feedback-absorption-ledger/TEMPLATE.md +69 -0
- package/.aict/mechanisms/half-product-review/EXAMPLE.synthetic.md +15 -0
- package/.aict/mechanisms/half-product-review/FAILURE_MODES.md +16 -0
- package/.aict/mechanisms/half-product-review/PROMPT.md +41 -0
- package/.aict/mechanisms/half-product-review/README.md +30 -0
- package/.aict/mechanisms/half-product-review/TEMPLATE.md +38 -0
- package/.aict/mechanisms/handoff-abc/EXAMPLE.synthetic.md +47 -0
- package/.aict/mechanisms/handoff-abc/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/handoff-abc/PROMPT.md +75 -0
- package/.aict/mechanisms/handoff-abc/README.md +82 -0
- package/.aict/mechanisms/handoff-abc/TEMPLATE.md +60 -0
- package/.aict/mechanisms/harvest-and-erc/EXAMPLE.synthetic.md +43 -0
- package/.aict/mechanisms/harvest-and-erc/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/harvest-and-erc/PROMPT.md +74 -0
- package/.aict/mechanisms/harvest-and-erc/README.md +81 -0
- package/.aict/mechanisms/harvest-and-erc/TEMPLATE.md +60 -0
- package/.aict/mechanisms/honest-calibration/EXAMPLE.synthetic.md +43 -0
- package/.aict/mechanisms/honest-calibration/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/honest-calibration/PROMPT.md +74 -0
- package/.aict/mechanisms/honest-calibration/README.md +81 -0
- package/.aict/mechanisms/honest-calibration/TEMPLATE.md +66 -0
- package/.aict/mechanisms/one-click-dispatch/EXAMPLE.synthetic.md +15 -0
- package/.aict/mechanisms/one-click-dispatch/FAILURE_MODES.md +16 -0
- package/.aict/mechanisms/one-click-dispatch/PROMPT.md +41 -0
- package/.aict/mechanisms/one-click-dispatch/README.md +30 -0
- package/.aict/mechanisms/one-click-dispatch/TEMPLATE.md +38 -0
- package/.aict/mechanisms/plain-language-first-screen/EXAMPLE.synthetic.md +15 -0
- package/.aict/mechanisms/plain-language-first-screen/FAILURE_MODES.md +16 -0
- package/.aict/mechanisms/plain-language-first-screen/PROMPT.md +41 -0
- package/.aict/mechanisms/plain-language-first-screen/README.md +30 -0
- package/.aict/mechanisms/plain-language-first-screen/TEMPLATE.md +38 -0
- package/.aict/mechanisms/root-cause-brake/EXAMPLE.synthetic.md +55 -0
- package/.aict/mechanisms/root-cause-brake/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/root-cause-brake/PROMPT.md +73 -0
- package/.aict/mechanisms/root-cause-brake/README.md +79 -0
- package/.aict/mechanisms/root-cause-brake/TEMPLATE.md +74 -0
- package/.aict/mechanisms/scout-review-controller/EXAMPLE.synthetic.md +15 -0
- package/.aict/mechanisms/scout-review-controller/FAILURE_MODES.md +16 -0
- package/.aict/mechanisms/scout-review-controller/PROMPT.md +41 -0
- package/.aict/mechanisms/scout-review-controller/README.md +30 -0
- package/.aict/mechanisms/scout-review-controller/TEMPLATE.md +38 -0
- package/.aict/mechanisms/single-tool-guard/EXAMPLE.synthetic.md +54 -0
- package/.aict/mechanisms/single-tool-guard/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/single-tool-guard/PROMPT.md +76 -0
- package/.aict/mechanisms/single-tool-guard/README.md +83 -0
- package/.aict/mechanisms/single-tool-guard/TEMPLATE.md +75 -0
- package/.aict/mechanisms/task-splitting/EXAMPLE.synthetic.md +53 -0
- package/.aict/mechanisms/task-splitting/FAILURE_MODES.md +25 -0
- package/.aict/mechanisms/task-splitting/PROMPT.md +72 -0
- package/.aict/mechanisms/task-splitting/README.md +79 -0
- package/.aict/mechanisms/task-splitting/TEMPLATE.md +76 -0
- package/.aict/modes/README.md +11 -0
- package/.aict/modes/execute.md +31 -0
- package/.aict/modes/handoff.md +29 -0
- package/.aict/modes/harvest.md +30 -0
- package/.aict/modes/review.md +28 -0
- package/.aict/modes/shape.md +34 -0
- package/.aict/privacy/COMMERCIAL_BOUNDARY.md +34 -0
- package/.aict/privacy/PRIVACY.md +36 -0
- package/.aict/privacy/REDACTION_CHECKLIST.md +12 -0
- package/.aict/profile/CANDIDATES.md +44 -0
- package/.aict/profile/EXAMPLE.synthetic.md +49 -0
- package/.aict/profile/FAILURE_MODES.md +40 -0
- package/.aict/profile/PROMPT.md +47 -0
- package/.aict/profile/README.md +44 -0
- package/.aict/profile/TEMPLATE.md +57 -0
- package/.aict/prompts/acceptance-definition.md +109 -0
- package/.aict/prompts/guard-review.md +116 -0
- package/.aict/prompts/handoff-generation.md +110 -0
- package/.aict/prompts/harvest-extraction.md +110 -0
- package/.aict/prompts/mode-switching.md +66 -0
- package/.aict/prompts/profile-creation.md +66 -0
- package/.aict/prompts/profile-refinement.md +66 -0
- package/.aict/prompts/project-context-packaging.md +113 -0
- package/.aict/prompts/red-team-challenge.md +106 -0
- package/.aict/prompts/rule-update-proposal.md +114 -0
- package/.aict/prompts/workflow-reset.md +109 -0
- package/.aict/roles/README.md +18 -0
- package/.aict/roles/executor.md +34 -0
- package/.aict/roles/harvester.md +33 -0
- package/.aict/roles/owner-controller.md +38 -0
- package/.aict/roles/scout.md +33 -0
- package/.aict/roles/supervisor.md +34 -0
- package/.aict/roles/system-guardian.md +34 -0
- package/.aict/skills/acceptance/SKILL.md +43 -0
- package/.aict/skills/context/SKILL.md +44 -0
- package/.aict/skills/evidence-pack/SKILL.md +42 -0
- package/.aict/skills/guard/SKILL.md +46 -0
- package/.aict/skills/handoff/SKILL.md +44 -0
- package/.aict/skills/harvest/SKILL.md +44 -0
- package/.aict/skills/mode-switch/SKILL.md +42 -0
- package/.aict/skills/profile/SKILL.md +42 -0
- package/.aict/skills/red-team/SKILL.md +42 -0
- package/.aict/skills/single-tool-guard/SKILL.md +42 -0
- package/.aict/state/CURRENT_STATE.md +13 -0
- package/.aict/state/DECISIONS.md +7 -0
- package/.aict/state/TASK_LOG.md +7 -0
- package/.aict/state/evidence.jsonl +2 -0
- package/.aict/state/learning-ledger.jsonl +1 -0
- package/.aict/state/receipts.jsonl +1 -0
- package/.aict/state/runs.jsonl +1 -0
- package/.aict/state/tasks.jsonl +1 -0
- package/.aict/walkthroughs/10-minute-your-task.md +107 -0
- package/.aict/walkthroughs/10-minute.md +43 -0
- package/.aict/walkthroughs/30-minute.md +22 -0
- package/.aict/walkthroughs/60-minute.md +27 -0
- package/.aict/walkthroughs/synthetic-loop-transcript.md +43 -0
- package/CHANGELOG.md +23 -0
- package/CODE_OF_CONDUCT.md +20 -0
- package/CONTRIBUTING.md +30 -0
- package/KNOWN_LIMITATIONS.md +54 -0
- package/LICENSE +199 -0
- package/PRODUCT_CONTRACT.md +446 -0
- package/README.md +245 -0
- package/RELEASE_CHECKLIST.md +78 -0
- package/SECURITY.md +56 -0
- package/START_HERE.md +89 -0
- package/bin/ai-collab.js +2 -0
- package/docs/DOGFOOD.md +85 -0
- package/docs/FEEDBACK.md +61 -0
- package/docs/FIRST_EXPERIENCE_SPEC.md +32 -0
- package/docs/FREE_VS_PAID.md +53 -0
- package/docs/PUBLIC_BOUNDARY.md +36 -0
- package/docs/PUBLIC_MAPPING.md +178 -0
- package/docs/RELEASE_PRIORITY.md +23 -0
- package/docs/WHY_THIS_EXISTS.md +36 -0
- package/docs/open-system/00-start-here.md +60 -0
- package/docs/open-system/01-ai-collaboration-os.md +33 -0
- package/docs/open-system/02-six-layer-architecture.md +45 -0
- package/docs/open-system/03-role-system.md +33 -0
- package/docs/open-system/04-core-mechanisms.md +34 -0
- package/docs/open-system/05-failure-patterns.md +31 -0
- package/docs/open-system/06-how-to-adapt-to-your-workflow.md +31 -0
- package/package.json +69 -0
- package/privacy-manifest.json +78 -0
- package/privacy-scan.local.json.example +18 -0
- package/scripts/lib/forbidden-in-pack.js +55 -0
- package/scripts/pack-check.js +154 -0
- package/scripts/privacy-scan.js +487 -0
- package/scripts/validate-contract.js +160 -0
- package/src/adapters.js +590 -0
- package/src/bootstrap.js +1184 -0
- package/src/catalog.js +2723 -0
- package/src/cli.js +2899 -0
- package/src/dialogue.js +470 -0
- package/src/i18n.js +1034 -0
- package/src/ledger.js +2011 -0
- package/src/render.js +1381 -0
- package/src/sendmodel.js +452 -0
- package/src/validate.js +1307 -0
- package/src/workspace.js +1679 -0
- package/tests/contract.test.js +8514 -0
package/src/adapters.js
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { renderSharedCoreContract } from "./render.js";
|
|
4
|
+
import { readLedger, confirmedProfileLearnings } from "./ledger.js";
|
|
5
|
+
|
|
6
|
+
const sixLayerProtocol = [
|
|
7
|
+
"Profile: capture stable collaboration preferences before long or recurring work.",
|
|
8
|
+
"Context: package the current task boundary, facts, assumptions, risks, and open questions.",
|
|
9
|
+
"Acceptance: define observable pass criteria before asking an AI tool to execute.",
|
|
10
|
+
"Guard: review artifacts against requirements, evidence, privacy, and scope before trust.",
|
|
11
|
+
"Handoff: leave the next session a concise state card with done, pending, blocked, and unverified work.",
|
|
12
|
+
"Harvest: extract reusable lessons, prompt fragments, and future rule candidates after a loop."
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// Single source of truth: the contract body embedded in every entrypoint is the
|
|
16
|
+
// exact text rendered into .aict/adapters/SHARED_CORE_CONTRACT.md by render.js.
|
|
17
|
+
// We inline it (instead of pointing at a file that `adapters install` does not
|
|
18
|
+
// create) so the rules — coaching layer, completion-claim guard routing
|
|
19
|
+
// (single-tool-guard / dual-guard / full fusion), restraint tiers, the first-run
|
|
20
|
+
// promise, and the six-layer core loop — are live the moment the tool reads its
|
|
21
|
+
// always-on instructions, with or without a generated workspace. Reusing
|
|
22
|
+
// renderSharedCoreContract() keeps this from drifting into a second copy.
|
|
23
|
+
function sharedCoreContractBody() {
|
|
24
|
+
return renderSharedCoreContract()
|
|
25
|
+
.replace(/^# Shared Core Contract\n+/, "")
|
|
26
|
+
.trimEnd();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- Confirmed-preference injection (the "white-filled profile" fix) ---------
|
|
30
|
+
//
|
|
31
|
+
// Filling a profile preference and confirming it used to only echo back in the
|
|
32
|
+
// tool's OWN `status` line — the rule files a real tool reads (CLAUDE.md,
|
|
33
|
+
// .cursorrules, AGENTS.md, ...) never carried it, so the AI actually doing the
|
|
34
|
+
// work could not see it. That made a confirmed preference feel adopted while
|
|
35
|
+
// being invisible to the assistant. This block closes that gap: at
|
|
36
|
+
// `adapters install` time we read the user's KEPT profile preferences straight
|
|
37
|
+
// from the learning ledger (the machine source of truth) and inline them into
|
|
38
|
+
// every generated rule file, so the assistant reads them as part of its
|
|
39
|
+
// always-on instructions.
|
|
40
|
+
//
|
|
41
|
+
// Honesty rules (the whole point of the proposed/confirmed buffer):
|
|
42
|
+
// - ONLY confirmed/edited rows are injected. A `proposed` row is an unreviewed
|
|
43
|
+
// guess; injecting it would pass an unconfirmed guess off as a standing rule.
|
|
44
|
+
// confirmedProfileLearnings() enforces this (it filters on the same
|
|
45
|
+
// isGraduatedLearningStatus the status recall uses).
|
|
46
|
+
// - When there are NONE, we do NOT fake one: the block says "No confirmed
|
|
47
|
+
// preferences yet" and points at how to add one, instead of a placeholder
|
|
48
|
+
// pseudo-preference.
|
|
49
|
+
// - The injected text is the ledger content VERBATIM (only outer whitespace is
|
|
50
|
+
// trimmed) — never paraphrased into a stronger or different instruction.
|
|
51
|
+
// - We read the ledger, never CANDIDATES.md, which self-declares it can drift
|
|
52
|
+
// from the ledger; the ledger is the source of truth.
|
|
53
|
+
|
|
54
|
+
// Resolve the workspace state dir for an adapters-install target. The target a
|
|
55
|
+
// user passes is normally their project root, whose workspace lives at
|
|
56
|
+
// `<target>/.aict` (with state under `.aict/state`); a user may also point
|
|
57
|
+
// `--target` straight at the `.aict` workspace dir itself. We mirror the CLI's
|
|
58
|
+
// own workspace detection (WORKSPACE_MANIFEST.json is the marker every generated
|
|
59
|
+
// `.aict` carries, and ONLY there) so this never drifts from where the run-layer
|
|
60
|
+
// commands read/write the ledger. We deliberately do NOT key off START_HERE.md:
|
|
61
|
+
// that doc also ships at the project root, so a bare-START_HERE.md probe matches
|
|
62
|
+
// the root and resolves state to <root>/state instead of <root>/.aict/state.
|
|
63
|
+
// Returns null when no real workspace is present — then there simply is no
|
|
64
|
+
// confirmed preference to inject (a fresh project that never ran `init`), and we
|
|
65
|
+
// say so rather than inventing one.
|
|
66
|
+
function resolveWorkspaceStateDir(targetRoot) {
|
|
67
|
+
const root = path.resolve(targetRoot);
|
|
68
|
+
if (existsSync(path.join(root, "WORKSPACE_MANIFEST.json"))) {
|
|
69
|
+
return path.join(root, "state");
|
|
70
|
+
}
|
|
71
|
+
if (existsSync(path.join(root, ".aict", "WORKSPACE_MANIFEST.json"))) {
|
|
72
|
+
return path.join(root, ".aict", "state");
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Read the user's KEPT profile preferences (confirmed/edited profile-type
|
|
78
|
+
// learning rows) for an adapters-install target, as the array of their `content`
|
|
79
|
+
// strings (trimmed, in ledger order). Safe-by-default: a target with no
|
|
80
|
+
// workspace, or a workspace whose learning ledger is missing/empty, yields []
|
|
81
|
+
// (no confirmed preferences — we will not fake any). A CORRUPT ledger is treated
|
|
82
|
+
// the same way (empty) instead of crashing the whole install: injecting the
|
|
83
|
+
// preference block is a courtesy on top of writing the rule files, and a broken
|
|
84
|
+
// ledger is surfaced by `validate`/`status`, not by refusing to lay down adapter
|
|
85
|
+
// guidance. Reuses readLedger + confirmedProfileLearnings so the read shape and
|
|
86
|
+
// the "what counts as kept" rule stay single-sourced with the rest of the CLI.
|
|
87
|
+
function readConfirmedPreferences(targetRoot) {
|
|
88
|
+
const stateDir = resolveWorkspaceStateDir(targetRoot);
|
|
89
|
+
if (!stateDir) return [];
|
|
90
|
+
let records;
|
|
91
|
+
try {
|
|
92
|
+
records = readLedger(stateDir, "learning");
|
|
93
|
+
} catch {
|
|
94
|
+
return []; // corrupt ledger: don't block the install; don't invent preferences
|
|
95
|
+
}
|
|
96
|
+
return confirmedProfileLearnings(records).map((row) => String(row.content).trim());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Render the "Your confirmed preferences" section injected into every rule file.
|
|
100
|
+
// `preferences` is the array of confirmed/edited preference strings from
|
|
101
|
+
// readConfirmedPreferences(). With one or more, each is a verbatim bullet so the
|
|
102
|
+
// assistant reads exactly what the user kept. With NONE, the section honestly
|
|
103
|
+
// says so and shows how to add one — it never emits a placeholder preference, so
|
|
104
|
+
// a fresh workspace's rule files carry no fake standing rule.
|
|
105
|
+
function renderConfirmedPreferencesSection(preferences) {
|
|
106
|
+
const heading = "## Your confirmed preferences";
|
|
107
|
+
if (!Array.isArray(preferences) || preferences.length === 0) {
|
|
108
|
+
return `${heading}
|
|
109
|
+
|
|
110
|
+
No confirmed preferences yet. These are the standing collaboration preferences the user has reviewed and kept; once any exist they are listed here for you to follow. To add one: \`ai-collab learning add --type profile --content "..."\` then \`ai-collab learning confirm --id <id>\` (only confirmed/edited preferences appear here — an unreviewed guess never does).`;
|
|
111
|
+
}
|
|
112
|
+
const bullets = preferences.map((line) => `- ${line}`).join("\n");
|
|
113
|
+
return `${heading}
|
|
114
|
+
|
|
115
|
+
These are standing collaboration preferences the user has reviewed and confirmed. Treat them as always-on instructions for how to work with this user, alongside the shared core contract below:
|
|
116
|
+
|
|
117
|
+
${bullets}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Each entrypoint carries a short tool `key` (the value `--tool` selects on) plus
|
|
121
|
+
// the relative file it writes and a `detect` list: marker files/dirs whose
|
|
122
|
+
// presence in the target means that tool is already in use here. `--tool auto`
|
|
123
|
+
// installs only the entrypoints whose detect markers are found, so a fresh
|
|
124
|
+
// install does not silently scatter six instruction files into a project that
|
|
125
|
+
// only uses one tool. `--tool all` keeps the original behavior (write all six).
|
|
126
|
+
//
|
|
127
|
+
// Detection-marker rule: a `detect` marker must be SPECIFIC to its tool, never a
|
|
128
|
+
// generic dir most repos already have. The cautionary case is Copilot: its
|
|
129
|
+
// marker is `.github/copilot-instructions.md` (the tool's own file), NOT the bare
|
|
130
|
+
// `.github/` dir — nearly every GitHub repo has `.github/` for workflows/issue
|
|
131
|
+
// templates, so detecting on the directory would auto-"find" Copilot everywhere
|
|
132
|
+
// and pollute unrelated repos. The bias is intentional: prefer "this tool's
|
|
133
|
+
// user was not auto-detected, so tell them to pass --tool" over "a plain repo
|
|
134
|
+
// got a wall of files it never asked for."
|
|
135
|
+
const adapterEntrypoints = [
|
|
136
|
+
{
|
|
137
|
+
key: "codex",
|
|
138
|
+
relativePath: "AGENTS.md",
|
|
139
|
+
tool: "Codex / AGENTS.md",
|
|
140
|
+
// AGENTS.md is now a vendor-neutral agent-instruction standard (Codex is its
|
|
141
|
+
// origin/primary consumer, but Cursor, Copilot, Jules, etc. also read it).
|
|
142
|
+
// It is still a precise "an AI agent is configured here" signal — unlike a
|
|
143
|
+
// bare framework dir, a repo only has AGENTS.md if someone added agent
|
|
144
|
+
// instructions — so it is a safe auto marker; we just install the Codex
|
|
145
|
+
// entrypoint (which writes AGENTS.md) for it. Users wanting a different tool
|
|
146
|
+
// can always pass --tool explicitly.
|
|
147
|
+
detect: ["AGENTS.md"]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
key: "claude",
|
|
151
|
+
relativePath: "CLAUDE.md",
|
|
152
|
+
tool: "Claude Code / CLAUDE.md",
|
|
153
|
+
detect: ["CLAUDE.md", ".claude"]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
key: "cursor",
|
|
157
|
+
relativePath: ".cursor/rules/ai-collab.mdc",
|
|
158
|
+
tool: "Cursor rules",
|
|
159
|
+
detect: [".cursor", ".cursorrules"]
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
key: "copilot",
|
|
163
|
+
relativePath: ".github/copilot-instructions.md",
|
|
164
|
+
tool: "GitHub Copilot instructions",
|
|
165
|
+
// Detect Copilot by its OWN instruction file, never by the bare `.github/`
|
|
166
|
+
// directory: almost every GitHub repo has `.github/` (workflows, issue
|
|
167
|
+
// templates, CODEOWNERS), so detecting on the directory would make the
|
|
168
|
+
// default `--tool auto` "find" Copilot in nearly every repo and write
|
|
169
|
+
// `.github/copilot-instructions.md` into projects that do not use Copilot.
|
|
170
|
+
detect: [".github/copilot-instructions.md"]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
key: "cline",
|
|
174
|
+
relativePath: ".clinerules",
|
|
175
|
+
tool: "Cline rules",
|
|
176
|
+
detect: [".clinerules"]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
key: "windsurf",
|
|
180
|
+
relativePath: ".windsurf/rules/ai-collab.md",
|
|
181
|
+
tool: "Windsurf rules",
|
|
182
|
+
detect: [".windsurf"]
|
|
183
|
+
}
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
export const adapterToolKeys = adapterEntrypoints.map((entry) => entry.key);
|
|
187
|
+
|
|
188
|
+
// Parse the --tool value into a concrete set of tool keys (or the symbolic
|
|
189
|
+
// "all" / "auto" selectors). Accepts a comma-separated list, validates every
|
|
190
|
+
// token, and throws on an unknown tool so a typo fails loudly instead of
|
|
191
|
+
// silently installing nothing.
|
|
192
|
+
export function parseToolSelection(rawValue) {
|
|
193
|
+
const value = (rawValue ?? "auto").trim();
|
|
194
|
+
if (value === "") return { mode: "auto", keys: [] };
|
|
195
|
+
const tokens = value
|
|
196
|
+
.split(",")
|
|
197
|
+
.map((token) => token.trim().toLowerCase())
|
|
198
|
+
.filter((token) => token.length > 0);
|
|
199
|
+
if (tokens.length === 0) return { mode: "auto", keys: [] };
|
|
200
|
+
|
|
201
|
+
if (tokens.includes("all")) return { mode: "all", keys: [...adapterToolKeys] };
|
|
202
|
+
if (tokens.includes("auto")) {
|
|
203
|
+
if (tokens.length > 1) {
|
|
204
|
+
throw new Error(`--tool auto cannot be combined with other tools (got: ${value}).`);
|
|
205
|
+
}
|
|
206
|
+
return { mode: "auto", keys: [] };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const known = new Set(adapterToolKeys);
|
|
210
|
+
const selected = [];
|
|
211
|
+
for (const token of tokens) {
|
|
212
|
+
if (!known.has(token)) {
|
|
213
|
+
throw new Error(`Unknown --tool "${token}". Valid: ${adapterToolKeys.join(", ")}, all, auto.`);
|
|
214
|
+
}
|
|
215
|
+
if (!selected.includes(token)) selected.push(token);
|
|
216
|
+
}
|
|
217
|
+
return { mode: "explicit", keys: selected };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Auto-detection: a tool is "present" when any of its marker files/dirs exists
|
|
221
|
+
// in the target. Returns the matched tool keys (in canonical order).
|
|
222
|
+
export function detectTools(targetRoot) {
|
|
223
|
+
const root = path.resolve(targetRoot);
|
|
224
|
+
return adapterEntrypoints
|
|
225
|
+
.filter((entry) => entry.detect.some((marker) => existsSync(path.join(root, marker))))
|
|
226
|
+
.map((entry) => entry.key);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function ensureDir(dir) {
|
|
230
|
+
mkdirSync(dir, { recursive: true });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function writeText(file, content) {
|
|
234
|
+
ensureDir(path.dirname(file));
|
|
235
|
+
writeFileSync(file, `${content.trimEnd()}\n`, "utf8");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function formatTimestamp(date = new Date()) {
|
|
239
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
240
|
+
return [
|
|
241
|
+
date.getFullYear(),
|
|
242
|
+
pad(date.getMonth() + 1),
|
|
243
|
+
pad(date.getDate())
|
|
244
|
+
].join("") + "-" + [pad(date.getHours()), pad(date.getMinutes()), pad(date.getSeconds())].join("");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function backupPathFor(file) {
|
|
248
|
+
const base = `${file}.aict-backup-${formatTimestamp()}`;
|
|
249
|
+
if (!existsSync(base)) return base;
|
|
250
|
+
for (let index = 2; index < 100; index += 1) {
|
|
251
|
+
const candidate = `${base}-${index}`;
|
|
252
|
+
if (!existsSync(candidate)) return candidate;
|
|
253
|
+
}
|
|
254
|
+
throw new Error(`Could not choose a backup path for ${file}.`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderAdapterEntrypoint(entry, preferences = []) {
|
|
258
|
+
return `# AI Collaboration Open System Adapter Guidance
|
|
259
|
+
|
|
260
|
+
This file is adapter guidance for ${entry.tool}. It is not a deep integration, background agent, telemetry hook, or hosted memory service.
|
|
261
|
+
|
|
262
|
+
This file is self-contained: the full shared core contract is embedded below, so the rules are live as soon as the tool reads its always-on instructions — you do not need to open any other file first. If you also ran \`node bin/ai-collab.js init --target <dir>\` (after the package is published to npm: \`ai-collab init --target <dir>\`), the deeper layer templates and examples live in that local \`.aict/\` workspace (\`.aict/profile\`, \`.aict/context\`, \`.aict/acceptance\`, \`.aict/guard\`, \`.aict/handoff\`, \`.aict/harvest\`, and \`.aict/mechanisms\`); this contract is the same one written to \`.aict/adapters/SHARED_CORE_CONTRACT.md\`.
|
|
263
|
+
|
|
264
|
+
${renderConfirmedPreferencesSection(preferences)}
|
|
265
|
+
|
|
266
|
+
## Six-layer core protocol
|
|
267
|
+
|
|
268
|
+
${sixLayerProtocol.map((line) => `- ${line}`).join("\n")}
|
|
269
|
+
|
|
270
|
+
## Adaptation tiers (least to most invasive)
|
|
271
|
+
|
|
272
|
+
- Rules (this file): the default, always-safe tier. Just guidance the tool reads; no automation.
|
|
273
|
+
- Skills (optional): reusable ability cards under \`.aict/skills/\` you load into a tool on demand after running \`init\`.
|
|
274
|
+
- Hooks (opt-in, off by default): \`adapters install --enable-hooks\` can add ONE project-local Claude Code Stop hook that reminds you to capture a receipt when you claim a task is done. It is never a global hook, the install lists every file first, and it is removable.
|
|
275
|
+
|
|
276
|
+
## Minimal operating rule
|
|
277
|
+
|
|
278
|
+
1. Follow the shared core contract embedded below as the single rule source for every tool — do not maintain a separate, drifting rule set.
|
|
279
|
+
2. Keep private material local. Do not upload files or infer hidden memory.
|
|
280
|
+
3. Label facts, assumptions, decisions, open questions, and unverified claims.
|
|
281
|
+
4. Before claiming completion, cite the acceptance card or say what remains unverified.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
${sharedCoreContractBody()}
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Honesty boundary
|
|
290
|
+
|
|
291
|
+
This adapter guidance only gives the tool a shared workflow. It does not synchronize memory across tools, automate every AI client, or guarantee better output without user review.
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- Optional hook layer (the "spearhead" demo; OFF by default) -------------
|
|
296
|
+
//
|
|
297
|
+
// This is the third, most invasive adaptation tier, layered ABOVE the always-safe
|
|
298
|
+
// rules entrypoints and the opt-in skills. It is gated behind an explicit
|
|
299
|
+
// --enable-hooks: the SECURITY / PRODUCT_CONTRACT promise is "no hooks without
|
|
300
|
+
// consent and never a global hook", so installAdapters() never touches this tier
|
|
301
|
+
// unless the caller turns it on. When on, it merges ONE Claude Code project-LOCAL
|
|
302
|
+
// Stop hook into the target's own .claude/settings.json (never the user's
|
|
303
|
+
// home/global config) that reminds the assistant to capture evidence + run
|
|
304
|
+
// `ai-collab receipt create` when it claims a task is done — the run-layer's
|
|
305
|
+
// completion checkpoint, surfaced at the exact moment a completion claim happens.
|
|
306
|
+
//
|
|
307
|
+
// Design: the reminder is INLINED into the settings command (a single self-
|
|
308
|
+
// contained `printf ... 1>&2; exit 0`) rather than shelling out to a separate
|
|
309
|
+
// script in the standard hooks directory. That keeps the install to one file, and
|
|
310
|
+
// — deliberately — avoids ever writing the standard `<claude-dir>/hooks` path
|
|
311
|
+
// literal, which the project's own privacy scanner flags as a leaked personal
|
|
312
|
+
// hook config. So a user who installs this hook and then runs the privacy scan on
|
|
313
|
+
// their own project still passes; the generated settings reference only the local
|
|
314
|
+
// dir name, not the hooks subpath.
|
|
315
|
+
const CLAUDE_LOCAL_DIR = ".claude";
|
|
316
|
+
// A stable marker tagged onto the Stop hook entry so it can be found and removed
|
|
317
|
+
// again (the "uninstallable" guarantee) without disturbing any other hooks the
|
|
318
|
+
// project already configured.
|
|
319
|
+
const HOOK_MARKER = "ai-collab-receipt-reminder";
|
|
320
|
+
|
|
321
|
+
function hookSettingsRelPosix() {
|
|
322
|
+
return [CLAUDE_LOCAL_DIR, "settings.json"].join("/");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// The inlined Stop-hook command. It is intentionally tiny and side-effect-free:
|
|
326
|
+
// it only prints guidance to stderr (which Claude Code surfaces from a Stop hook)
|
|
327
|
+
// and exits 0, so enabling it can never block the assistant from stopping or
|
|
328
|
+
// mutate any file. No absolute paths, tokens, script files, or private material —
|
|
329
|
+
// it is fully generic and self-contained.
|
|
330
|
+
function hookCommandString() {
|
|
331
|
+
const lines = [
|
|
332
|
+
"[ai-collab] Claimed a task is done? Capture evidence + a receipt:",
|
|
333
|
+
" ai-collab evidence add --task <id> --kind output --summary <text>",
|
|
334
|
+
" ai-collab receipt create --task <id> --verdict <pass|reject|insufficient_evidence|pass_with_risk> --guard-level <L0-L4>"
|
|
335
|
+
];
|
|
336
|
+
return `printf '%s\\n' ${lines.map((line) => `'${line}'`).join(" ")} 1>&2; exit 0`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function hookEntry() {
|
|
340
|
+
return {
|
|
341
|
+
// Tag the matcher with our marker so the entry is identifiable + removable.
|
|
342
|
+
matcher: HOOK_MARKER,
|
|
343
|
+
hooks: [
|
|
344
|
+
{
|
|
345
|
+
type: "command",
|
|
346
|
+
command: hookCommandString(),
|
|
347
|
+
timeout: 5000,
|
|
348
|
+
statusMessage: "ai-collab: remind to capture evidence + receipt"
|
|
349
|
+
}
|
|
350
|
+
]
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// True if a parsed settings object already has our marked Stop hook, so a repeat
|
|
355
|
+
// --enable-hooks is idempotent (we do not append a duplicate entry).
|
|
356
|
+
function hasOurStopHook(settings) {
|
|
357
|
+
const stop = settings?.hooks?.Stop;
|
|
358
|
+
if (!Array.isArray(stop)) return false;
|
|
359
|
+
return stop.some((entry) => entry && entry.matcher === HOOK_MARKER);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Merge our Stop hook into an existing settings object WITHOUT dropping anything
|
|
363
|
+
// the project already configured: other events, other Stop entries, and all other
|
|
364
|
+
// keys are preserved; we only append our marked entry to hooks.Stop.
|
|
365
|
+
function mergeStopHook(settings) {
|
|
366
|
+
const next = settings && typeof settings === "object" ? { ...settings } : {};
|
|
367
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? { ...next.hooks } : {};
|
|
368
|
+
const stop = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
|
|
369
|
+
stop.push(hookEntry());
|
|
370
|
+
hooks.Stop = stop;
|
|
371
|
+
next.hooks = hooks;
|
|
372
|
+
return next;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Plan the hook-layer writes for the target. Hooks are Claude-Code-specific, so
|
|
376
|
+
// they only apply when the claude entrypoint is in the selected set. Returns a
|
|
377
|
+
// list of planned file actions (create / merge / backup-replace / skip) plus a
|
|
378
|
+
// reason when nothing is planned, so the CLI can explain exactly what (if
|
|
379
|
+
// anything) --enable-hooks will do — including in --dry-run.
|
|
380
|
+
export function plannedHookActions(targetRoot, selectedKeys) {
|
|
381
|
+
const root = path.resolve(targetRoot);
|
|
382
|
+
const keys = new Set(selectedKeys ?? []);
|
|
383
|
+
if (!keys.has("claude")) {
|
|
384
|
+
return { applicable: false, reason: "hooks are Claude Code only; select --tool claude (or a set including it) to enable them", actions: [] };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const actions = [];
|
|
388
|
+
const settingsFull = path.join(root, CLAUDE_LOCAL_DIR, "settings.json");
|
|
389
|
+
let settingsAction = "create";
|
|
390
|
+
if (existsSync(settingsFull)) {
|
|
391
|
+
let parsed = null;
|
|
392
|
+
try {
|
|
393
|
+
parsed = JSON.parse(readFileSync(settingsFull, "utf8"));
|
|
394
|
+
} catch {
|
|
395
|
+
parsed = undefined; // unparseable -> we will not clobber it; surface as a skip
|
|
396
|
+
}
|
|
397
|
+
if (parsed === undefined) {
|
|
398
|
+
settingsAction = "skip-unparseable";
|
|
399
|
+
} else if (hasOurStopHook(parsed)) {
|
|
400
|
+
settingsAction = "already-present";
|
|
401
|
+
} else {
|
|
402
|
+
settingsAction = "merge";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
actions.push({
|
|
406
|
+
kind: "hook-settings",
|
|
407
|
+
relativePath: hookSettingsRelPosix(),
|
|
408
|
+
path: settingsFull,
|
|
409
|
+
action: settingsAction
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
return { applicable: true, reason: null, actions };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Execute the planned hook writes. Mirrors installAdapters' safety posture:
|
|
416
|
+
// existing files are backed up before replacement, an unparseable settings.json
|
|
417
|
+
// is left untouched (reported, not clobbered), and a settings.json that already
|
|
418
|
+
// carries our marked entry is left as-is (idempotent).
|
|
419
|
+
function applyHookActions(targetRoot, plan, { dryRun }) {
|
|
420
|
+
const root = path.resolve(targetRoot);
|
|
421
|
+
const written = [];
|
|
422
|
+
const backups = [];
|
|
423
|
+
|
|
424
|
+
if (dryRun || !plan.applicable) {
|
|
425
|
+
return { written, backups };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (const item of plan.actions) {
|
|
429
|
+
if (item.kind === "hook-settings") {
|
|
430
|
+
if (item.action === "skip-unparseable" || item.action === "already-present") {
|
|
431
|
+
continue; // never clobber an unparseable file; never duplicate our entry
|
|
432
|
+
}
|
|
433
|
+
if (item.action === "create") {
|
|
434
|
+
ensureDir(path.dirname(item.path));
|
|
435
|
+
writeFileSync(item.path, `${JSON.stringify(mergeStopHook({}), null, 2)}\n`, "utf8");
|
|
436
|
+
written.push(item.relativePath);
|
|
437
|
+
} else if (item.action === "merge") {
|
|
438
|
+
const existing = JSON.parse(readFileSync(item.path, "utf8"));
|
|
439
|
+
const backup = backupPathFor(item.path);
|
|
440
|
+
renameSync(item.path, backup);
|
|
441
|
+
backups.push(backup);
|
|
442
|
+
writeFileSync(item.path, `${JSON.stringify(mergeStopHook(existing), null, 2)}\n`, "utf8");
|
|
443
|
+
written.push(item.relativePath);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return { written, backups };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Resolve which entrypoints to install. `selectedKeys` is the concrete tool-key
|
|
452
|
+
// list (already expanded from --tool by parseToolSelection / detectTools).
|
|
453
|
+
// Each returned entry tags its `action`: "create" (no existing file) or
|
|
454
|
+
// "backup-replace" (a file already exists and would be backed up first), so the
|
|
455
|
+
// CLI can list exactly which paths are created vs replaced — including in
|
|
456
|
+
// --dry-run, where nothing is actually written.
|
|
457
|
+
export function plannedAdapterEntrypoints(target, selectedKeys) {
|
|
458
|
+
const targetRoot = path.resolve(target);
|
|
459
|
+
const keys = selectedKeys ?? adapterToolKeys;
|
|
460
|
+
const keySet = new Set(keys);
|
|
461
|
+
// Read the user's confirmed/edited profile preferences ONCE (not per file) so
|
|
462
|
+
// every rule file we generate carries the same kept-preference block. When the
|
|
463
|
+
// target has no workspace / no kept preference, this is [] and the block
|
|
464
|
+
// honestly says "No confirmed preferences yet" (see renderConfirmedPreferencesSection).
|
|
465
|
+
const preferences = readConfirmedPreferences(targetRoot);
|
|
466
|
+
return adapterEntrypoints
|
|
467
|
+
.filter((entry) => keySet.has(entry.key))
|
|
468
|
+
.map((entry) => {
|
|
469
|
+
const fullPath = path.join(targetRoot, entry.relativePath);
|
|
470
|
+
return {
|
|
471
|
+
key: entry.key,
|
|
472
|
+
tool: entry.tool,
|
|
473
|
+
relativePath: entry.relativePath,
|
|
474
|
+
path: fullPath,
|
|
475
|
+
action: existsSync(fullPath) ? "backup-replace" : "create",
|
|
476
|
+
content: renderAdapterEntrypoint(entry, preferences)
|
|
477
|
+
};
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Decide the effective tool key set from the --tool option. `auto` (the default)
|
|
482
|
+
// resolves to the detected tools; when nothing is detected it returns an empty
|
|
483
|
+
// set plus `autoFoundNothing: true` so the caller can prompt the user to pass an
|
|
484
|
+
// explicit --tool instead of silently scattering all six files.
|
|
485
|
+
export function resolveToolSelection(targetRoot, toolOption) {
|
|
486
|
+
const selection = parseToolSelection(toolOption);
|
|
487
|
+
if (selection.mode === "auto") {
|
|
488
|
+
const detected = detectTools(targetRoot);
|
|
489
|
+
return { mode: "auto", keys: detected, detected, autoFoundNothing: detected.length === 0 };
|
|
490
|
+
}
|
|
491
|
+
return { mode: selection.mode, keys: selection.keys, detected: null, autoFoundNothing: false };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function installAdapters(target, options = {}) {
|
|
495
|
+
const targetRoot = path.resolve(target);
|
|
496
|
+
const resolution = resolveToolSelection(targetRoot, options.tool);
|
|
497
|
+
const entries = plannedAdapterEntrypoints(targetRoot, resolution.keys);
|
|
498
|
+
|
|
499
|
+
// Opt-in hook layer (OFF unless --enable-hooks). Plan it here so the same plan
|
|
500
|
+
// is reported in dry-run and executed in a real run; nothing is planned when
|
|
501
|
+
// the flag is off, so the rules/skills tiers are entirely unaffected.
|
|
502
|
+
const hookPlan = options.enableHooks
|
|
503
|
+
? plannedHookActions(targetRoot, resolution.keys)
|
|
504
|
+
: { applicable: false, reason: "hooks not requested (pass --enable-hooks to opt in)", actions: [] };
|
|
505
|
+
|
|
506
|
+
// Auto mode with no tool detected: do not write anything. Tell the caller to
|
|
507
|
+
// pick a tool explicitly (or `--tool all`). This is surfaced (not thrown) so
|
|
508
|
+
// both dry-run and real install report it cleanly.
|
|
509
|
+
if (resolution.autoFoundNothing) {
|
|
510
|
+
return {
|
|
511
|
+
targetRoot,
|
|
512
|
+
files: 0,
|
|
513
|
+
dryRun: Boolean(options.dryRun),
|
|
514
|
+
written: false,
|
|
515
|
+
backups: [],
|
|
516
|
+
toolMode: resolution.mode,
|
|
517
|
+
detected: resolution.detected,
|
|
518
|
+
autoFoundNothing: true,
|
|
519
|
+
plan: [],
|
|
520
|
+
hooks: { enabled: Boolean(options.enableHooks), applicable: false, reason: hookPlan.reason, plan: [], written: [] }
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const plan = entries.map((entry) => ({
|
|
525
|
+
tool: entry.tool,
|
|
526
|
+
relativePath: entry.relativePath,
|
|
527
|
+
path: entry.path,
|
|
528
|
+
action: entry.action
|
|
529
|
+
}));
|
|
530
|
+
|
|
531
|
+
if (options.dryRun) {
|
|
532
|
+
return {
|
|
533
|
+
targetRoot,
|
|
534
|
+
files: entries.length,
|
|
535
|
+
dryRun: true,
|
|
536
|
+
written: false,
|
|
537
|
+
backups: [],
|
|
538
|
+
toolMode: resolution.mode,
|
|
539
|
+
detected: resolution.detected,
|
|
540
|
+
autoFoundNothing: false,
|
|
541
|
+
plan,
|
|
542
|
+
hooks: {
|
|
543
|
+
enabled: Boolean(options.enableHooks),
|
|
544
|
+
applicable: hookPlan.applicable,
|
|
545
|
+
reason: hookPlan.reason,
|
|
546
|
+
plan: hookPlan.actions.map((item) => ({ relativePath: item.relativePath, action: item.action })),
|
|
547
|
+
written: []
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const collisions = entries.filter((entry) => existsSync(entry.path));
|
|
553
|
+
if (collisions.length > 0 && !options.force) {
|
|
554
|
+
throw new Error(
|
|
555
|
+
`Adapter file already exists: ${collisions[0].relativePath}. Pass --force to back up and replace adapter guidance.`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const backups = [];
|
|
560
|
+
for (const entry of entries) {
|
|
561
|
+
if (existsSync(entry.path)) {
|
|
562
|
+
const backup = backupPathFor(entry.path);
|
|
563
|
+
renameSync(entry.path, backup);
|
|
564
|
+
backups.push(backup);
|
|
565
|
+
}
|
|
566
|
+
writeText(entry.path, entry.content);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const hookResult = applyHookActions(targetRoot, hookPlan, { dryRun: false });
|
|
570
|
+
backups.push(...hookResult.backups);
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
targetRoot,
|
|
574
|
+
files: entries.length,
|
|
575
|
+
dryRun: false,
|
|
576
|
+
written: true,
|
|
577
|
+
backups,
|
|
578
|
+
toolMode: resolution.mode,
|
|
579
|
+
detected: resolution.detected,
|
|
580
|
+
autoFoundNothing: false,
|
|
581
|
+
plan,
|
|
582
|
+
hooks: {
|
|
583
|
+
enabled: Boolean(options.enableHooks),
|
|
584
|
+
applicable: hookPlan.applicable,
|
|
585
|
+
reason: hookPlan.reason,
|
|
586
|
+
plan: hookPlan.actions.map((item) => ({ relativePath: item.relativePath, action: item.action })),
|
|
587
|
+
written: hookResult.written
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|