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/bootstrap.js
ADDED
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
// === bootstrap (first-experience value report) ==============================
|
|
2
|
+
//
|
|
3
|
+
// `bootstrap` is the first-experience entry point. The problem it solves: a new
|
|
4
|
+
// user who just ran `init` sees a 200-file framework and does NOT know what it is
|
|
5
|
+
// for — the value is never proven on their OWN work. bootstrap reads what the
|
|
6
|
+
// user actually has locally (their repo, their git activity, their .aict ledger,
|
|
7
|
+
// their AI instruction files) and turns it into ONE plain "AI collaboration
|
|
8
|
+
// baseline report": five cards — PROFILE CLUES (setup signals to confirm),
|
|
9
|
+
// VERIFY (which "done"s cannot be trusted yet), RESUME (where you are / what is
|
|
10
|
+
// missing), ROLES (high-risk keywords mapped to helper roles), HARVEST (what
|
|
11
|
+
// you can carry forward).
|
|
12
|
+
//
|
|
13
|
+
// HONESTY IS THE WHOLE POINT (four red lines, enforced structurally here):
|
|
14
|
+
// 1. Deterministic only. This module reads files + runs read-only git; it calls
|
|
15
|
+
// NO external model, makes NO guess, and sends NOTHING anywhere.
|
|
16
|
+
// 2. A completion claim is NEVER shown as verified/done unless the ledger's OWN
|
|
17
|
+
// honest functions say it is. Every guard level / family marker is RE-COMPUTED
|
|
18
|
+
// by the shared ledger.js functions (computeReceiptGuardLevel /
|
|
19
|
+
// buildHandoffModel / summarizeTasks) — this file rewrites NONE of that logic,
|
|
20
|
+
// so bootstrap can never look cleaner than `status` / `check` / `handoff`.
|
|
21
|
+
// 3. HARVEST candidates are PROPOSED. bootstrap (report-only) writes NOTHING to
|
|
22
|
+
// a profile or any long-term state; it only lists structural facts.
|
|
23
|
+
// 4. The shipped synthetic seed is never counted as the user's own work
|
|
24
|
+
// (isSeedRow excludes it), so an empty workspace honestly says "no data yet"
|
|
25
|
+
// instead of borrowing the example's numbers.
|
|
26
|
+
//
|
|
27
|
+
// This is the report-only version: scan + five cards + a consent preview + init
|
|
28
|
+
// tie-in. It now ALSO does the LOCAL HALF of semantic scanning (dialogue.js): when —
|
|
29
|
+
// and ONLY when — the user EXPLICITLY hands over a local chat/log export
|
|
30
|
+
// (`--dialogue` / `--logs`), bootstrap reads it and extracts DETERMINISTIC signals
|
|
31
|
+
// (a word-table completion claim cross-referenced against the ledger; a repeated
|
|
32
|
+
// correction) to enrich the five cards. That local half stays inside red line #1:
|
|
33
|
+
// no model call, no guess, no network — a "done" found in a chat becomes a VERIFY
|
|
34
|
+
// CANDIDATE labelled "claimed in dialogue · not verified", never a "done". The
|
|
35
|
+
// EXTERNAL-model half (`--send-to-model`), a save/write-back flow, and a GUI are
|
|
36
|
+
// still deliberately out of scope (a later sub-batch). See the TODO markers below.
|
|
37
|
+
|
|
38
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import {
|
|
41
|
+
summarizeTasks,
|
|
42
|
+
buildHandoffModel,
|
|
43
|
+
isSeedRow,
|
|
44
|
+
familyHonestyMarker
|
|
45
|
+
} from "./ledger.js";
|
|
46
|
+
import { detectTools } from "./adapters.js";
|
|
47
|
+
import { t } from "./i18n.js";
|
|
48
|
+
|
|
49
|
+
// --- A. Local structure scan (read-only, zero network) ----------------------
|
|
50
|
+
|
|
51
|
+
// Read a JSON file, returning null on any problem (missing / unparseable). Never
|
|
52
|
+
// throws — a scan must degrade gracefully, not abort the whole report.
|
|
53
|
+
function readJsonSafe(file) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// True when a path exists and is a directory (used to confirm a .git or .aict).
|
|
62
|
+
function isDir(p) {
|
|
63
|
+
try {
|
|
64
|
+
return statSync(p).isDirectory();
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Find the nearest ancestor directory (including `start`) that contains a `.git`,
|
|
71
|
+
// i.e. the git working-tree root. Returns null when none is found (not a repo).
|
|
72
|
+
// Pure filesystem walk — no git invocation, so it works even where git is absent.
|
|
73
|
+
function findGitRoot(start) {
|
|
74
|
+
let dir = path.resolve(start);
|
|
75
|
+
for (;;) {
|
|
76
|
+
if (existsSync(path.join(dir, ".git"))) return dir;
|
|
77
|
+
const parent = path.dirname(dir);
|
|
78
|
+
if (parent === dir) return null; // reached filesystem root
|
|
79
|
+
dir = parent;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The recognized AI instruction-file markers, surfaced in the scan so the report
|
|
84
|
+
// can say "your repo already talks to <tools>". REUSES the adapters detect logic
|
|
85
|
+
// (detectTools) so bootstrap and `adapters install` agree on what counts as an AI
|
|
86
|
+
// instruction file — single source, no second list to drift.
|
|
87
|
+
function scanAiInstructionFiles(root) {
|
|
88
|
+
const detected = detectTools(root); // e.g. ["claude", "codex"]
|
|
89
|
+
// A few well-known top-level instruction files, reported individually so the
|
|
90
|
+
// user sees the concrete filename. detectTools already drives the load-bearing
|
|
91
|
+
// "which tools" answer; this is only for a friendly per-file presence list.
|
|
92
|
+
const KNOWN_FILES = ["CLAUDE.md", "AGENTS.md", "README.md", ".cursorrules", ".clinerules"];
|
|
93
|
+
const files = KNOWN_FILES.filter((name) => existsSync(path.join(root, name)));
|
|
94
|
+
return { detectedTools: detected, instructionFiles: files };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Read package.json scripts + a best-effort "test entry" (the npm `test` script,
|
|
98
|
+
// when present). Returns { present, scripts: string[], testScript: string|null }.
|
|
99
|
+
function scanPackageJson(root) {
|
|
100
|
+
const pkgPath = path.join(root, "package.json");
|
|
101
|
+
if (!existsSync(pkgPath)) return { present: false, scripts: [], testScript: null };
|
|
102
|
+
const pkg = readJsonSafe(pkgPath);
|
|
103
|
+
if (!pkg || typeof pkg !== "object") return { present: false, scripts: [], testScript: null };
|
|
104
|
+
const scriptsObj = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
105
|
+
const scripts = Object.keys(scriptsObj);
|
|
106
|
+
const testScript = typeof scriptsObj.test === "string" ? scriptsObj.test : null;
|
|
107
|
+
return { present: true, scripts, testScript };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// List up to `limit` of the most-recently-modified files in the repo root (one
|
|
111
|
+
// level only — a shallow, fast, deterministic-ish signal of "what you touched
|
|
112
|
+
// last"), skipping noise dirs. mtime is a heuristic, surfaced as such. Read-only.
|
|
113
|
+
function scanRecentlyModified(root, limit = 8) {
|
|
114
|
+
const SKIP = new Set([".git", "node_modules", ".aict"]);
|
|
115
|
+
let entries;
|
|
116
|
+
try {
|
|
117
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
118
|
+
} catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
const rows = [];
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (entry.name.startsWith(".") && entry.name !== ".cursorrules" && entry.name !== ".clinerules") {
|
|
124
|
+
// skip dotfiles/dirs by default (they are rarely the "what I am working on"
|
|
125
|
+
// signal), except the two instruction dotfiles we already care about.
|
|
126
|
+
if (SKIP.has(entry.name)) continue;
|
|
127
|
+
}
|
|
128
|
+
if (SKIP.has(entry.name)) continue;
|
|
129
|
+
if (!entry.isFile()) continue;
|
|
130
|
+
const full = path.join(root, entry.name);
|
|
131
|
+
try {
|
|
132
|
+
const st = statSync(full);
|
|
133
|
+
rows.push({ name: entry.name, mtimeMs: st.mtimeMs });
|
|
134
|
+
} catch {
|
|
135
|
+
/* unreadable: skip */
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
139
|
+
return rows.slice(0, limit).map((r) => r.name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Parse the porcelain-ish output of `git log --name-only -n N` (passed in by the
|
|
143
|
+
// CLI, which owns the spawn) into a count of how many of the last N commits each
|
|
144
|
+
// file appeared in. A file touched in MANY recent commits is a candidate "kept
|
|
145
|
+
// re-fixing the same thing" signal — a possible "said done but still patching it"
|
|
146
|
+
// tell, which is exactly the kind of un-trustworthy completion bootstrap exists to
|
|
147
|
+
// surface. Pure string parsing here; the git call is the CLI's job (so this stays
|
|
148
|
+
// testable without a real repo).
|
|
149
|
+
//
|
|
150
|
+
// `logText` is the raw stdout. Lines that look like a path (no leading "commit ",
|
|
151
|
+
// not blank, not an author/date header) are counted. We deliberately count
|
|
152
|
+
// DISTINCT commits per file, not raw line repeats.
|
|
153
|
+
export function parseRepeatedlyTouchedFiles(logText, { minCommits = 3, limit = 5 } = {}) {
|
|
154
|
+
if (typeof logText !== "string" || logText.trim().length === 0) return [];
|
|
155
|
+
// Split into per-commit blocks on the "commit <sha>" boundary that
|
|
156
|
+
// `git log --name-only` prints. The first block may be empty.
|
|
157
|
+
const blocks = logText.split(/^commit [0-9a-f]+/m);
|
|
158
|
+
const counts = new Map();
|
|
159
|
+
for (const block of blocks) {
|
|
160
|
+
const filesInCommit = new Set();
|
|
161
|
+
for (const rawLine of block.split("\n")) {
|
|
162
|
+
const line = rawLine.trim();
|
|
163
|
+
if (line.length === 0) continue;
|
|
164
|
+
// Skip the standard headers git emits inside a commit block.
|
|
165
|
+
if (/^Author:/.test(line) || /^Date:/.test(line) || /^Merge:/.test(line)) continue;
|
|
166
|
+
// A commit message body is indented by 4 spaces in `git log`; a changed path
|
|
167
|
+
// is NOT indented (name-only prints bare paths). Use the RAW line's leading
|
|
168
|
+
// whitespace to tell them apart, and require a path-ish shape.
|
|
169
|
+
if (/^\s/.test(rawLine)) continue; // indented => message text, not a path
|
|
170
|
+
if (!/[\w./-]/.test(line)) continue;
|
|
171
|
+
if (line.includes(" ") && !line.includes("/") && !line.includes(".")) continue;
|
|
172
|
+
filesInCommit.add(line);
|
|
173
|
+
}
|
|
174
|
+
for (const file of filesInCommit) {
|
|
175
|
+
counts.set(file, (counts.get(file) ?? 0) + 1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return [...counts.entries()]
|
|
179
|
+
.filter(([, n]) => n >= minCommits)
|
|
180
|
+
.sort((a, b) => b[1] - a[1])
|
|
181
|
+
.slice(0, limit)
|
|
182
|
+
.map(([file, commits]) => ({ file, commits }));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Assemble the full structural scan from the pieces the CLI gathers. `git` carries
|
|
186
|
+
// the read-only git output the CLI already captured (log/diff text + whether git
|
|
187
|
+
// ran), so this module never spawns a process itself.
|
|
188
|
+
//
|
|
189
|
+
// workspaceRoot: the .aict workspace dir (…/.aict)
|
|
190
|
+
// repoRoot: the project root the user is in (holds package.json, the repo)
|
|
191
|
+
// git: { available, logText, diffStatText } captured by the CLI
|
|
192
|
+
//
|
|
193
|
+
// Returns a plain, serializable scan object (no I/O beyond reads done here).
|
|
194
|
+
export function scanLocalStructure({ workspaceRoot, repoRoot, git = {} }) {
|
|
195
|
+
const root = path.resolve(repoRoot);
|
|
196
|
+
const gitRoot = findGitRoot(root);
|
|
197
|
+
const pkg = scanPackageJson(root);
|
|
198
|
+
const ai = scanAiInstructionFiles(root);
|
|
199
|
+
const recentlyModified = scanRecentlyModified(root);
|
|
200
|
+
const repeatedlyTouched = parseRepeatedlyTouchedFiles(typeof git.logText === "string" ? git.logText : "");
|
|
201
|
+
const diffStat = typeof git.diffStatText === "string" ? git.diffStatText.trim() : "";
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
repoRoot: root,
|
|
205
|
+
workspaceRoot: workspaceRoot ? path.resolve(workspaceRoot) : null,
|
|
206
|
+
git: {
|
|
207
|
+
available: git.available === true,
|
|
208
|
+
isRepo: gitRoot !== null,
|
|
209
|
+
root: gitRoot,
|
|
210
|
+
// The presence of an uncommitted diff is a "work in flight" signal for RESUME.
|
|
211
|
+
hasUncommittedChanges: diffStat.length > 0,
|
|
212
|
+
diffStat,
|
|
213
|
+
repeatedlyTouched
|
|
214
|
+
},
|
|
215
|
+
packageJson: pkg,
|
|
216
|
+
ai,
|
|
217
|
+
recentlyModified
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- B. The trust-bearing cards (the core value), built from the HONEST ledger functions ---
|
|
222
|
+
//
|
|
223
|
+
// Everything trust-bearing below is DERIVED by the shared ledger.js functions, not
|
|
224
|
+
// recomputed here: buildHandoffModel does the done/unverified bucketing with the
|
|
225
|
+
// recomputed guard level + familyUnverified marker; summarizeTasks gives the
|
|
226
|
+
// per-task "author-marked, unverified" flag. The actual per-receipt level math
|
|
227
|
+
// (computeReceiptGuardLevel) runs INSIDE those ledger functions — bootstrap calls
|
|
228
|
+
// them and reads their output; it never calls the level computer itself. bootstrap
|
|
229
|
+
// only SELECTS and LABELS — it owns no level math.
|
|
230
|
+
|
|
231
|
+
// A short, stable reason code + plain sentence for each kind of un-trustworthy
|
|
232
|
+
// completion claim VERIFY surfaces. The phrasing avoids jargon on the first read.
|
|
233
|
+
const VERIFY_REASON = {
|
|
234
|
+
pending_receipt: "a review exists but is still pending — not accepted",
|
|
235
|
+
pass_with_risk_unaccepted: "passed only with a noted risk that no one has signed off on yet",
|
|
236
|
+
self_declared_cross_family: "self-declared cross-family review — the tool cannot verify the other model actually checked it",
|
|
237
|
+
author_marked_done: "marked done by the author with no accepted review behind it"
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Build the VERIFY card: the completion claims that CANNOT be trusted as done yet.
|
|
241
|
+
// Sources (all recomputed honestly by buildHandoffModel / summarizeTasks):
|
|
242
|
+
// - every task in the handoff model's `unverified` bucket (pending pass, a
|
|
243
|
+
// pass_with_risk still pending, OR an accepted-but-self-declared-cross-family
|
|
244
|
+
// receipt — buildHandoffModel routes all of these here using the RE-COMPUTED
|
|
245
|
+
// level, never a stored flag);
|
|
246
|
+
// - any task flagged authorMarkedDoneUnverified (status done, no done-eligible
|
|
247
|
+
// accepted receipt — the note-only "done");
|
|
248
|
+
// - a self-declared cross-family marker is called out explicitly even when the
|
|
249
|
+
// receipt was locally accepted, so it is shown "unverified", never "done".
|
|
250
|
+
// Each item carries the task id/title, the offending receipt (with its RE-COMPUTED
|
|
251
|
+
// level + verdict + status), a reason code, and an explicit `displayedAsDone:false`
|
|
252
|
+
// so a consumer can assert bootstrap never renders these as completed.
|
|
253
|
+
function buildVerifyCard(handoff, perTask, dialogue = null) {
|
|
254
|
+
const items = [];
|
|
255
|
+
const seen = new Set(); // de-dupe (taskId, receiptId, reason)
|
|
256
|
+
|
|
257
|
+
const pushItem = (task, receiptView, reasonCode) => {
|
|
258
|
+
const key = `${task.id}|${receiptView ? receiptView.id : "-"}|${reasonCode}`;
|
|
259
|
+
if (seen.has(key)) return;
|
|
260
|
+
seen.add(key);
|
|
261
|
+
items.push({
|
|
262
|
+
// `source: "ledger"` marks the trust-bearing items that come from the user's
|
|
263
|
+
// OWN recorded work (the receipts/runs the tool itself wrote). Dialogue-sourced
|
|
264
|
+
// items below carry `source: "dialogue"` so a renderer/consumer can keep the two
|
|
265
|
+
// visually + structurally SEPARATE (red line: a chat claim is never mixed into
|
|
266
|
+
// the ledger facts as if it were verified).
|
|
267
|
+
source: "ledger",
|
|
268
|
+
taskId: task.id,
|
|
269
|
+
title: task.title,
|
|
270
|
+
taskStatus: task.taskStatus ?? task.status,
|
|
271
|
+
// The receipt is described by its RE-COMPUTED level (buildHandoffModel /
|
|
272
|
+
// summarizeTasks already recompute it); we copy those fields, never a stored
|
|
273
|
+
// guardLevel. receiptView may be null for a note-only done with no receipt.
|
|
274
|
+
receipt: receiptView
|
|
275
|
+
? {
|
|
276
|
+
id: receiptView.id,
|
|
277
|
+
verdict: receiptView.verdict,
|
|
278
|
+
guardLevel: receiptView.guardLevel, // RE-COMPUTED upstream
|
|
279
|
+
status: receiptView.status,
|
|
280
|
+
familyUnverified: receiptView.familyUnverified === true
|
|
281
|
+
}
|
|
282
|
+
: null,
|
|
283
|
+
reason: reasonCode,
|
|
284
|
+
reasonText: VERIFY_REASON[reasonCode] ?? reasonCode,
|
|
285
|
+
// The load-bearing honesty assertion: bootstrap NEVER displays a VERIFY item
|
|
286
|
+
// as done/verified. Carried explicitly so a test (and a renderer) can rely on it.
|
|
287
|
+
displayedAsDone: false,
|
|
288
|
+
// The self-declared-cross-family marker text, when applicable, surfaced
|
|
289
|
+
// verbatim from the shared helper so the wording matches the rest of the tool.
|
|
290
|
+
familyMarker: receiptView && receiptView.familyUnverified === true
|
|
291
|
+
? familyHonestyMarker(true)
|
|
292
|
+
: null
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// 1) Everything the handoff model already bucketed as Unverified. Each such task
|
|
297
|
+
// has >=1 receipt that is NOT done-eligible. We classify the strongest reason
|
|
298
|
+
// per receipt from the RE-COMPUTED view buildHandoffModel handed us.
|
|
299
|
+
for (const entry of handoff.unverified) {
|
|
300
|
+
const receipts = entry.receipts ?? [];
|
|
301
|
+
if (receipts.length === 0) {
|
|
302
|
+
// Reviewed-but-not-accepted with no receipt view should not happen (the
|
|
303
|
+
// bucket implies a receipt), but guard anyway: mark the task itself.
|
|
304
|
+
pushItem(entry, null, "author_marked_done");
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
for (const r of receipts) {
|
|
308
|
+
if (r.familyUnverified === true) {
|
|
309
|
+
pushItem(entry, r, "self_declared_cross_family");
|
|
310
|
+
} else if (r.verdict === "pass_with_risk" && r.status !== "accepted") {
|
|
311
|
+
pushItem(entry, r, "pass_with_risk_unaccepted");
|
|
312
|
+
} else if (r.status === "pending") {
|
|
313
|
+
pushItem(entry, r, "pending_receipt");
|
|
314
|
+
} else if (r.status === "rejected") {
|
|
315
|
+
// a rejected receipt is also "not done"; surface it as pending-style review
|
|
316
|
+
pushItem(entry, r, "pending_receipt");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 2) A task in ANY bucket whose only/loudest problem is an accepted-but-self-
|
|
322
|
+
// declared-cross-family receipt: buildHandoffModel routes a task whose ONLY
|
|
323
|
+
// acceptances are familyUnverified into `unverified` (handled above), but a
|
|
324
|
+
// task that is Done on a clean receipt AND also carries a separate self-declared
|
|
325
|
+
// cross-family receipt keeps a riskNote — surface that receipt too, so a
|
|
326
|
+
// cross-family claim is never silently trusted. We scan done/blocked entries.
|
|
327
|
+
for (const entry of [...handoff.done, ...handoff.blocked, ...handoff.pending]) {
|
|
328
|
+
for (const r of entry.receipts ?? []) {
|
|
329
|
+
if (r.familyUnverified === true) {
|
|
330
|
+
pushItem(entry, r, "self_declared_cross_family");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 3) Note-only / author-marked done with no done-eligible accepted receipt — the
|
|
336
|
+
// thin "done". summarizeTasks recomputed authorMarkedDoneUnverified for us.
|
|
337
|
+
for (const t of perTask) {
|
|
338
|
+
if (t.isSeed) continue; // never the shipped example
|
|
339
|
+
if (t.authorMarkedDoneUnverified) {
|
|
340
|
+
// Use the strongest receipt view if there is one; else flag the bare task.
|
|
341
|
+
const rv = t.receipt
|
|
342
|
+
? {
|
|
343
|
+
id: t.receipt.id,
|
|
344
|
+
verdict: t.receipt.verdict,
|
|
345
|
+
guardLevel: t.receipt.guardLevel, // RE-COMPUTED in summarizeTasks
|
|
346
|
+
status: t.receipt.status,
|
|
347
|
+
familyUnverified: t.receipt.familyUnverified === true
|
|
348
|
+
}
|
|
349
|
+
: null;
|
|
350
|
+
pushItem(
|
|
351
|
+
{ id: t.id, title: t.title, taskStatus: t.statusDisplay ?? t.status },
|
|
352
|
+
rv,
|
|
353
|
+
"author_marked_done"
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 4) DIALOGUE-SOURCED completion claims (red line #2 + #4-visual-separation). Each
|
|
359
|
+
// is a deterministic word-table match from a LOCAL export the user handed over,
|
|
360
|
+
// cross-referenced against the ledger and found UNBACKED (no accepted clean
|
|
361
|
+
// receipt, no executed run). It is surfaced as a VERIFY FINDING with the exact
|
|
362
|
+
// wording "claimed in dialogue · not verified" and displayedAsDone:false — it is
|
|
363
|
+
// NEVER a task status, NEVER rendered as done, and is tagged source:"dialogue" so
|
|
364
|
+
// it is shown SEPARATE from the ledger facts above. Snippets are pre-redacted by
|
|
365
|
+
// dialogue.js (redactSnippet) before they ever reach here.
|
|
366
|
+
if (dialogue && Array.isArray(dialogue.suspectedFalseCompletions)) {
|
|
367
|
+
for (const claim of dialogue.suspectedFalseCompletions) {
|
|
368
|
+
items.push({
|
|
369
|
+
source: "dialogue",
|
|
370
|
+
// The stable, non-softenable label for a chat-sourced, unbacked completion
|
|
371
|
+
// claim. Carried verbatim so a test/renderer can assert it is never "done".
|
|
372
|
+
label: "claimed in dialogue · not verified",
|
|
373
|
+
sourcePath: claim.source,
|
|
374
|
+
line: claim.line,
|
|
375
|
+
subject: claim.subject,
|
|
376
|
+
snippet: claim.snippet, // already redacted + clamped by dialogue.js
|
|
377
|
+
confidence: claim.confidence ?? "low",
|
|
378
|
+
// The load-bearing honesty assertions, identical to the ledger items'.
|
|
379
|
+
displayedAsDone: false,
|
|
380
|
+
backed: false
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return items;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Build the RESUME card: where the user is and what is missing to pick back up.
|
|
389
|
+
// - activeTasks: open/partial tasks (NOT seed) — work in progress.
|
|
390
|
+
// - missingHandoff: true when there is in-progress/unverified work but NO handoff
|
|
391
|
+
// draft has been generated (the next session would start from zero).
|
|
392
|
+
// - gitDrift: the repeatedly-touched files (a "still churning the same files"
|
|
393
|
+
// signal) + whether there are uncommitted changes.
|
|
394
|
+
function buildResumeCard(handoff, perTask, scan, handoffDraftCount) {
|
|
395
|
+
const activeTasks = perTask
|
|
396
|
+
.filter((t) => !t.isSeed && (t.status === "open" || t.status === "partial"))
|
|
397
|
+
.map((t) => ({ id: t.id, title: t.title, status: t.status }));
|
|
398
|
+
|
|
399
|
+
const hasInFlightWork =
|
|
400
|
+
activeTasks.length > 0 ||
|
|
401
|
+
handoff.unverified.length > 0 ||
|
|
402
|
+
handoff.pending.length > 0 ||
|
|
403
|
+
scan.git.hasUncommittedChanges;
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
activeTasks,
|
|
407
|
+
inProgressCount: handoff.counts.pending + handoff.counts.unverified,
|
|
408
|
+
missingHandoff: hasInFlightWork && handoffDraftCount === 0,
|
|
409
|
+
handoffDraftCount,
|
|
410
|
+
gitDrift: {
|
|
411
|
+
repeatedlyTouched: scan.git.repeatedlyTouched,
|
|
412
|
+
hasUncommittedChanges: scan.git.hasUncommittedChanges
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Build the HARVEST card: what is safe to carry forward — DETERMINISTIC facts only.
|
|
418
|
+
// - confirmedLearnings: the confirmed/edited learning rows buildHandoffModel
|
|
419
|
+
// already collected (proposed/dropped excluded). These are facts the user
|
|
420
|
+
// themselves kept, so they are reported as-is.
|
|
421
|
+
// - candidates: STRUCTURAL, proposed-only observations (v1 lists NO LLM guess) —
|
|
422
|
+
// e.g. "N confirmed learnings ready to carry forward". Each is marked
|
|
423
|
+
// proposed:true; bootstrap (report-only) writes none of them anywhere.
|
|
424
|
+
function buildHarvestCard(handoff, learning = [], dialogue = null) {
|
|
425
|
+
const confirmedLearnings = (handoff.learnings ?? []).map((l) => ({
|
|
426
|
+
id: l.id,
|
|
427
|
+
type: l.type,
|
|
428
|
+
content: l.content,
|
|
429
|
+
status: l.status
|
|
430
|
+
}));
|
|
431
|
+
|
|
432
|
+
// Proposed (not-yet-kept) lessons of the user's OWN — the rows a "learning confirm
|
|
433
|
+
// <id>" next step can actually act on. The shipped example seed (l0) is excluded so
|
|
434
|
+
// the suggestion never pushes the user to confirm the demo lesson. Carried on the
|
|
435
|
+
// card model (not the rendered body) so the Next step can name a real id.
|
|
436
|
+
const proposedLearnings = (Array.isArray(learning) ? learning : [])
|
|
437
|
+
.filter((l) => l && l.status === "proposed" && !isSeedRow(l, "learning"))
|
|
438
|
+
.map((l) => ({ id: l.id, type: l.type, content: l.content, status: l.status }));
|
|
439
|
+
|
|
440
|
+
const candidates = [];
|
|
441
|
+
if (confirmedLearnings.length > 0) {
|
|
442
|
+
candidates.push({
|
|
443
|
+
kind: "confirmed_learnings_ready",
|
|
444
|
+
proposed: true,
|
|
445
|
+
// `count` is carried so the renderer can build the localized detail; `detail`
|
|
446
|
+
// stays canonical English in the model (a stable --json data contract).
|
|
447
|
+
count: confirmedLearnings.length,
|
|
448
|
+
detail: `${confirmedLearnings.length} confirmed learning${confirmedLearnings.length === 1 ? "" : "s"} you can carry into your next task`
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const doneCount = handoff.counts.done;
|
|
452
|
+
if (doneCount > 0) {
|
|
453
|
+
candidates.push({
|
|
454
|
+
kind: "verified_done_tasks",
|
|
455
|
+
proposed: true,
|
|
456
|
+
count: doneCount,
|
|
457
|
+
detail: `${doneCount} task${doneCount === 1 ? "" : "s"} reached a verified (accepted) result — a pattern worth reusing`
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// DIALOGUE-SOURCED profile candidates (red line #3 + #4-visual-separation). A
|
|
462
|
+
// correction the user repeated >= 2 times in their LOCAL export is a deterministic
|
|
463
|
+
// signal of a standing preference the AI keeps missing — a HARVEST *profile*
|
|
464
|
+
// candidate. It is PROPOSED only (proposed:true): this module writes NOTHING to a
|
|
465
|
+
// profile or ledger; confirming it is the user's explicit `learning confirm` step
|
|
466
|
+
// later. Tagged source:"dialogue" + type:"profile" so it is shown SEPARATE from the
|
|
467
|
+
// user's own confirmed learnings, and labelled as coming from the chat, unverified.
|
|
468
|
+
const dialogueCandidates = [];
|
|
469
|
+
if (dialogue && Array.isArray(dialogue.repeatedCorrections)) {
|
|
470
|
+
for (const corr of dialogue.repeatedCorrections) {
|
|
471
|
+
dialogueCandidates.push({
|
|
472
|
+
kind: "repeated_correction_profile",
|
|
473
|
+
source: "dialogue",
|
|
474
|
+
type: "profile",
|
|
475
|
+
proposed: true,
|
|
476
|
+
count: corr.count,
|
|
477
|
+
normalized: corr.normalized,
|
|
478
|
+
snippet: corr.snippet, // already redacted + clamped by dialogue.js
|
|
479
|
+
confidence: corr.confidence ?? "low",
|
|
480
|
+
detail: `a correction you repeated ${corr.count}× in your chat — a standing preference worth recording`
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return { confirmedLearnings, proposedLearnings, candidates, dialogueCandidates };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// --- B2. Profile-clues card (DETERMINISTIC signals only, NO semantic verdict) --
|
|
489
|
+
//
|
|
490
|
+
// The fourth card. Its ONLY job is to list CONCRETE, machine-observable facts the
|
|
491
|
+
// user can recognise — never a personality read. bootstrap is a report-only engine
|
|
492
|
+
// with no model and no right to GUESS a work style: it may say "you have Claude +
|
|
493
|
+
// Cursor configured here" (a fact the user can confirm by looking) but NOT "you
|
|
494
|
+
// prefer X" (a semantic judgment). The footer makes the contract explicit: these
|
|
495
|
+
// are CLUES the user's OWN ai will turn into a profile in first-run, not a
|
|
496
|
+
// conclusion bootstrap reached. Pure read over the scan; writes nothing, calls
|
|
497
|
+
// nothing.
|
|
498
|
+
//
|
|
499
|
+
// Signals (all from the existing scan, all deterministic):
|
|
500
|
+
// - detectedTools: the AI tools whose instruction files are present (reused from
|
|
501
|
+
// scanAiInstructionFiles -> detectTools). >= 2 is flagged as a cross-tool
|
|
502
|
+
// signal — a FACT (two tools are configured), not a claim about how they work.
|
|
503
|
+
// - fileTypes: the extension distribution of the recently-modified files (a count
|
|
504
|
+
// per extension). "you touched .ts and .sql files lately" is observable; what
|
|
505
|
+
// it MEANS is for the user's ai, not bootstrap.
|
|
506
|
+
// - hasTestScript: whether package.json has a `test` script. We label it the
|
|
507
|
+
// plain fact "has a test script" — a signal the user can verify, never
|
|
508
|
+
// "you value testing" stated as settled truth.
|
|
509
|
+
// Each datum carries the raw fact so a --json consumer (and a test) can assert the
|
|
510
|
+
// card contains evidence, not interpretation.
|
|
511
|
+
export function buildProfileCard(scan) {
|
|
512
|
+
const ai = scan && scan.ai ? scan.ai : { detectedTools: [], instructionFiles: [] };
|
|
513
|
+
const detectedTools = Array.isArray(ai.detectedTools) ? ai.detectedTools : [];
|
|
514
|
+
|
|
515
|
+
// Extension distribution of the recently-modified files. A file with no extension
|
|
516
|
+
// (e.g. "Makefile") is bucketed under "(no ext)" so the count stays honest. The
|
|
517
|
+
// list is sorted by count desc, then name, for a stable display + --json contract.
|
|
518
|
+
const recent = Array.isArray(scan && scan.recentlyModified) ? scan.recentlyModified : [];
|
|
519
|
+
const extCounts = new Map();
|
|
520
|
+
for (const name of recent) {
|
|
521
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
522
|
+
const ext = path.extname(name); // ".ts", "", ".sql", …
|
|
523
|
+
const key = ext.length > 0 ? ext : "(no ext)";
|
|
524
|
+
extCounts.set(key, (extCounts.get(key) ?? 0) + 1);
|
|
525
|
+
}
|
|
526
|
+
const fileTypes = [...extCounts.entries()]
|
|
527
|
+
.map(([ext, count]) => ({ ext, count }))
|
|
528
|
+
.sort((a, b) => b.count - a.count || a.ext.localeCompare(b.ext));
|
|
529
|
+
|
|
530
|
+
const hasTestScript =
|
|
531
|
+
Boolean(scan && scan.packageJson && scan.packageJson.present) &&
|
|
532
|
+
typeof scan.packageJson.testScript === "string" &&
|
|
533
|
+
scan.packageJson.testScript.length > 0;
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
// The detected tools, and whether there is more than one (a cross-tool FACT).
|
|
537
|
+
detectedTools,
|
|
538
|
+
multiTool: detectedTools.length >= 2,
|
|
539
|
+
fileTypes,
|
|
540
|
+
hasTestScript,
|
|
541
|
+
// The load-bearing honesty assertion, mirrored on every card item a consumer
|
|
542
|
+
// might mistake for a verdict: this card draws NO semantic conclusion. Carried
|
|
543
|
+
// so a test can assert bootstrap never crosses the report-only line here.
|
|
544
|
+
semanticConclusion: false
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// --- B3. Roles-suggestion card (DETERMINISTIC keyword match -> existing roles) --
|
|
549
|
+
//
|
|
550
|
+
// The fifth card. It scans the user's task titles + recently-touched file PATHS for
|
|
551
|
+
// a fixed list of high-risk keywords and, when one matches, suggests an EXISTING
|
|
552
|
+
// open-source role package to bring in. The honesty contract is the same as the
|
|
553
|
+
// profile card: it states a FACT ("the title 'payment callback' contains the word
|
|
554
|
+
// 'payment'") and a deterministic mapping to a role — it does NOT decide the work
|
|
555
|
+
// IS risky, only that a known high-risk WORD appears. The user's ai makes the call;
|
|
556
|
+
// the card's wording says these are clues to confirm. Pure read; no write, no model.
|
|
557
|
+
//
|
|
558
|
+
// The keyword table is grouped by RISK THEME; each theme maps to one or more role
|
|
559
|
+
// suggestions. The roles named here are all packages that already ship in the
|
|
560
|
+
// open-source workspace (.aict/skills/ + .aict/mechanisms/): red-team, dual-guard,
|
|
561
|
+
// scout-review-controller — verified present, never invented.
|
|
562
|
+
const ROLE_RISK_GROUPS = [
|
|
563
|
+
{
|
|
564
|
+
theme: "auth", // authentication / credentials
|
|
565
|
+
keywords: ["auth", "login", "password", "credential", "token", "secret"],
|
|
566
|
+
roles: ["red-team", "dual-guard"]
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
theme: "money", // payments / billing
|
|
570
|
+
keywords: ["payment", "pay", "billing", "invoice"],
|
|
571
|
+
roles: ["red-team", "dual-guard"]
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
theme: "security", // explicit security / crypto
|
|
575
|
+
keywords: ["security", "crypto"],
|
|
576
|
+
roles: ["scout-review-controller", "red-team"]
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
theme: "deploy", // deployment / data layer
|
|
580
|
+
keywords: ["deploy", "release", "migration", "database", "schema"],
|
|
581
|
+
roles: ["scout-review-controller", "red-team"]
|
|
582
|
+
}
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
// Find the FIRST keyword (lowercase substring) from `keywords` that appears in
|
|
586
|
+
// `haystack` (already lowercased). Returns the matched keyword or null. Substring
|
|
587
|
+
// match is deliberate so "authentication"/"reauth" trip "auth"; it is a CLUE the
|
|
588
|
+
// user's ai confirms, so a slightly broad match is acceptable (and honest about
|
|
589
|
+
// being a word-match, never a verdict).
|
|
590
|
+
function firstKeywordHit(haystack, keywords) {
|
|
591
|
+
for (const kw of keywords) {
|
|
592
|
+
if (haystack.includes(kw)) return kw;
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Build the roles card. `tasks` and `scan` come from the same inputs the other
|
|
598
|
+
// cards use. Seed tasks are excluded (isSeedRow) so the shipped example never
|
|
599
|
+
// manufactures a role suggestion. Returns:
|
|
600
|
+
// - hits: up to TOP 3 high-risk items, each { subject, keyword, theme, roles,
|
|
601
|
+
// source } — `subject` is the task title or file path that matched, so the card
|
|
602
|
+
// can show the exact evidence ("task 'X' matched keyword 'Y' -> suggest Z").
|
|
603
|
+
// - roles: the de-duplicated union of suggested role ids across the hits (for the
|
|
604
|
+
// Next-step + a --json consumer), each tagged with the plain-language simile key.
|
|
605
|
+
// - hasHighRisk: whether anything matched at all (drives the honest no-match line).
|
|
606
|
+
// The card NEVER says the work is risky; it says a high-risk WORD appears and maps
|
|
607
|
+
// it to a role to consider — a deterministic fact + a fixed mapping, no judgment.
|
|
608
|
+
export function buildRolesCard(tasks, scan) {
|
|
609
|
+
const taskList = Array.isArray(tasks) ? tasks : [];
|
|
610
|
+
const recent = Array.isArray(scan && scan.recentlyModified) ? scan.recentlyModified : [];
|
|
611
|
+
|
|
612
|
+
// The candidate surfaces to scan: every NON-seed task title (source "task") and
|
|
613
|
+
// every recently-modified file path (source "file"). Each carries the raw subject
|
|
614
|
+
// so the matched evidence is shown verbatim, never paraphrased into a conclusion.
|
|
615
|
+
const subjects = [];
|
|
616
|
+
for (const task of taskList) {
|
|
617
|
+
if (!task || typeof task !== "object") continue;
|
|
618
|
+
if (isSeedRow(task, "tasks")) continue; // never the shipped example
|
|
619
|
+
const title = typeof task.title === "string" ? task.title : "";
|
|
620
|
+
if (title.length === 0) continue;
|
|
621
|
+
subjects.push({ source: "task", subject: title });
|
|
622
|
+
}
|
|
623
|
+
for (const name of recent) {
|
|
624
|
+
if (typeof name === "string" && name.length > 0) {
|
|
625
|
+
subjects.push({ source: "file", subject: name });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Match each subject against every risk group; collect the hits. We keep the FIRST
|
|
630
|
+
// group that matches a given subject (a subject rarely belongs to two themes, and
|
|
631
|
+
// showing one clear reason per subject is less noisy than every partial match).
|
|
632
|
+
const hits = [];
|
|
633
|
+
for (const { subject, source } of subjects) {
|
|
634
|
+
const haystack = subject.toLowerCase();
|
|
635
|
+
for (const group of ROLE_RISK_GROUPS) {
|
|
636
|
+
const keyword = firstKeywordHit(haystack, group.keywords);
|
|
637
|
+
if (keyword) {
|
|
638
|
+
hits.push({ subject, source, keyword, theme: group.theme, roles: group.roles });
|
|
639
|
+
break; // one theme per subject
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Cap at TOP 3 high-risk items so the card does not carpet-bomb the user (the brief:
|
|
645
|
+
// "no repeated bombardment; at most top 3"). Order is scan order (tasks first, then
|
|
646
|
+
// files), which is stable and deterministic.
|
|
647
|
+
const topHits = hits.slice(0, 3);
|
|
648
|
+
|
|
649
|
+
// The de-duplicated union of suggested roles across the shown hits, each with the
|
|
650
|
+
// plain-language simile key the renderer/Next-step uses. Stable order = first seen.
|
|
651
|
+
const roleOrder = [];
|
|
652
|
+
for (const hit of topHits) {
|
|
653
|
+
for (const role of hit.roles) {
|
|
654
|
+
if (!roleOrder.includes(role)) roleOrder.push(role);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const roles = roleOrder.map((id) => ({ id }));
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
hasHighRisk: topHits.length > 0,
|
|
661
|
+
hits: topHits,
|
|
662
|
+
roles,
|
|
663
|
+
// The same report-only honesty flag the profile card carries: a keyword match is
|
|
664
|
+
// a fact + a fixed mapping, NOT a decision that the work is risky.
|
|
665
|
+
semanticConclusion: false
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Top-level: turn the scan + the raw ledgers into the full bootstrap model. The
|
|
670
|
+
// ledgers are passed in already-parsed (the CLI reads them via readLedger). We run
|
|
671
|
+
// buildHandoffModel + summarizeTasks ONCE and hand their honest output to the card
|
|
672
|
+
// builders. `hasOwnData` is the seed-honesty gate: TRUE only when there is at least
|
|
673
|
+
// one NON-seed row across the trust-bearing ledgers, so an empty/seed-only
|
|
674
|
+
// workspace is reported as "no data yet" instead of borrowing the example.
|
|
675
|
+
export function buildBootstrapModel({ ledgers, scan, handoffDraftCount = 0, dialogue = null }) {
|
|
676
|
+
const tasks = Array.isArray(ledgers.tasks) ? ledgers.tasks : [];
|
|
677
|
+
const evidence = Array.isArray(ledgers.evidence) ? ledgers.evidence : [];
|
|
678
|
+
const runs = Array.isArray(ledgers.runs) ? ledgers.runs : [];
|
|
679
|
+
const receipts = Array.isArray(ledgers.receipts) ? ledgers.receipts : [];
|
|
680
|
+
const learning = Array.isArray(ledgers.learning) ? ledgers.learning : [];
|
|
681
|
+
|
|
682
|
+
// The HONEST core: bucketing with re-computed levels (buildHandoffModel skips the
|
|
683
|
+
// seed task by default) + per-task achievement (summarizeTasks flags seeds).
|
|
684
|
+
const handoff = buildHandoffModel({ tasks, evidence, runs, receipts, learning });
|
|
685
|
+
const perTask = summarizeTasks(tasks, receipts, evidence, runs);
|
|
686
|
+
|
|
687
|
+
// Seed-honesty: count NON-seed rows. A brand-new workspace ships exactly one seed
|
|
688
|
+
// set (t0/e0/e1/r0/c0/l0); if every row is a seed, the user has no data of their
|
|
689
|
+
// own. We check each ledger with the shared isSeedRow so "what is a seed" stays
|
|
690
|
+
// defined in one place.
|
|
691
|
+
const nonSeed = (rows, key) => rows.filter((row) => !isSeedRow(row, key));
|
|
692
|
+
const ownTasks = nonSeed(tasks, "tasks");
|
|
693
|
+
const ownEvidence = nonSeed(evidence, "evidence");
|
|
694
|
+
const ownRuns = nonSeed(runs, "runs");
|
|
695
|
+
const ownReceipts = nonSeed(receipts, "receipts");
|
|
696
|
+
const ownLearning = nonSeed(learning, "learning");
|
|
697
|
+
const hasOwnData =
|
|
698
|
+
ownTasks.length > 0 ||
|
|
699
|
+
ownEvidence.length > 0 ||
|
|
700
|
+
ownRuns.length > 0 ||
|
|
701
|
+
ownReceipts.length > 0 ||
|
|
702
|
+
ownLearning.length > 0;
|
|
703
|
+
|
|
704
|
+
const verify = buildVerifyCard(handoff, perTask, dialogue);
|
|
705
|
+
const resume = buildResumeCard(handoff, perTask, scan, handoffDraftCount);
|
|
706
|
+
const harvest = buildHarvestCard(handoff, learning, dialogue);
|
|
707
|
+
// The two new DETERMINISTIC cards (red line: facts + recognisable evidence, never
|
|
708
|
+
// a semantic verdict). profile reads the scan only; roles reads the user's task
|
|
709
|
+
// titles (seeds excluded) + recently-touched file paths against a fixed high-risk
|
|
710
|
+
// keyword table. Neither writes or calls a model — pure reads, like the cards above.
|
|
711
|
+
const profile = buildProfileCard(scan);
|
|
712
|
+
const roles = buildRolesCard(tasks, scan);
|
|
713
|
+
|
|
714
|
+
// The dialogue scan is the user's OWN data (a chat THEY exported), so even a
|
|
715
|
+
// seed-only ledger gains a real baseline once a dialogue file is read — `dialogueUsed`
|
|
716
|
+
// lets the renderer show the cards (not the "no data yet" short-circuit) when there
|
|
717
|
+
// are dialogue findings, while the seed-honesty `hasOwnData` (ledger-only) is
|
|
718
|
+
// unchanged. Carried on the model so a --json consumer can branch on it too.
|
|
719
|
+
const dialogueUsed = dialogue && dialogue.used === true;
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
reportOnly: true, // report-only; this model never drives a write.
|
|
723
|
+
hasOwnData,
|
|
724
|
+
seedOnly: !hasOwnData,
|
|
725
|
+
dialogueUsed: dialogueUsed === true,
|
|
726
|
+
// The transparency record (red line #5): which local files were read, how many
|
|
727
|
+
// flagged snippets, and the explicit "all local, nothing sent" promise. null when
|
|
728
|
+
// no dialogue/log file was provided (so the default report is byte-identical).
|
|
729
|
+
dialogue: dialogue
|
|
730
|
+
? {
|
|
731
|
+
used: dialogue.used === true,
|
|
732
|
+
sources: dialogue.sources ?? [],
|
|
733
|
+
skipped: dialogue.skipped ?? [],
|
|
734
|
+
snippetCount: dialogue.snippetCount ?? 0
|
|
735
|
+
}
|
|
736
|
+
: null,
|
|
737
|
+
counts: {
|
|
738
|
+
ownTasks: ownTasks.length,
|
|
739
|
+
ownEvidence: ownEvidence.length,
|
|
740
|
+
ownRuns: ownRuns.length,
|
|
741
|
+
ownReceipts: ownReceipts.length,
|
|
742
|
+
ownLearning: ownLearning.length,
|
|
743
|
+
handoff: handoff.counts
|
|
744
|
+
},
|
|
745
|
+
scan,
|
|
746
|
+
// Card order in the model mirrors the render order: profile (a warm, factual
|
|
747
|
+
// opener) -> verify -> resume -> roles -> harvest. A --json consumer reads them
|
|
748
|
+
// by name, so the object-key order is for readability only.
|
|
749
|
+
cards: { profile, verify, resume, roles, harvest }
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// --- D. Render (plain language first; terms go in details) ------------------
|
|
754
|
+
//
|
|
755
|
+
// The first screen avoids jargon (no L0-L4 / "receipt" / "harvest" theory up top).
|
|
756
|
+
// Each card leads with a plain sentence; the term (guard level, etc.) rides along
|
|
757
|
+
// only as a parenthetical detail. All numbers come from the real scan/model — there
|
|
758
|
+
// is NO hard-coded demo copy.
|
|
759
|
+
|
|
760
|
+
function renderVerifyCard(verify, locale = "en") {
|
|
761
|
+
const lines = [];
|
|
762
|
+
lines.push(t("bootstrap.verify.title", {}, locale));
|
|
763
|
+
// Split the ledger-sourced (trust-bearing) items from the dialogue-sourced
|
|
764
|
+
// candidates so the two are NEVER mixed: ledger facts first, then a clearly
|
|
765
|
+
// separated "from your chat" block (red line #4 — visual separation).
|
|
766
|
+
const ledgerItems = verify.filter((v) => v.source !== "dialogue");
|
|
767
|
+
const dialogueItems = verify.filter((v) => v.source === "dialogue");
|
|
768
|
+
|
|
769
|
+
if (ledgerItems.length === 0 && dialogueItems.length === 0) {
|
|
770
|
+
lines.push(t("bootstrap.verify.allClear1", {}, locale));
|
|
771
|
+
lines.push(t("bootstrap.verify.allClear2", {}, locale));
|
|
772
|
+
return lines.join("\n");
|
|
773
|
+
}
|
|
774
|
+
if (ledgerItems.length > 0) {
|
|
775
|
+
lines.push(t("bootstrap.verify.count", { count: ledgerItems.length, plural: ledgerItems.length === 1 ? "" : "s" }, locale));
|
|
776
|
+
for (const item of ledgerItems) {
|
|
777
|
+
const title = item.title && item.title.length > 0 ? item.title : t("common.untitled", {}, locale);
|
|
778
|
+
// Reason is rendered from the STABLE reason CODE (item.reason), localized here;
|
|
779
|
+
// the model's reasonText stays canonical English (a data contract), the display
|
|
780
|
+
// is translated — including the honesty wording, faithfully, never softened.
|
|
781
|
+
const reasonText = t(`bootstrap.verify.reason.${item.reason}`, {}, locale);
|
|
782
|
+
lines.push(t("bootstrap.verify.item", { taskId: item.taskId, title, reason: reasonText }, locale));
|
|
783
|
+
if (item.receipt) {
|
|
784
|
+
// Term detail (kept on its own indented line): the receipt + its RE-COMPUTED level.
|
|
785
|
+
// The self-declared-cross-family marker is rendered in the active locale (the
|
|
786
|
+
// honesty caveat must read in the user's language, faithfully translated).
|
|
787
|
+
const marker = item.familyMarker
|
|
788
|
+
? ` [${t("marker.selfDeclaredCrossFamily", {}, locale)}]`
|
|
789
|
+
: "";
|
|
790
|
+
lines.push(
|
|
791
|
+
t("bootstrap.verify.detail", {
|
|
792
|
+
id: item.receipt.id,
|
|
793
|
+
verdict: item.receipt.verdict,
|
|
794
|
+
level: item.receipt.guardLevel,
|
|
795
|
+
status: item.receipt.status,
|
|
796
|
+
marker
|
|
797
|
+
}, locale)
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// The dialogue-sourced block: a separate header that names where it came from and
|
|
803
|
+
// that it is unverified, then each claim with the "claimed in dialogue · not
|
|
804
|
+
// verified" wording (localized but never softened) + its redacted snippet.
|
|
805
|
+
if (dialogueItems.length > 0) {
|
|
806
|
+
lines.push(t("bootstrap.verify.dialogueHead", { count: dialogueItems.length, plural: dialogueItems.length === 1 ? "" : "s" }, locale));
|
|
807
|
+
for (const item of dialogueItems) {
|
|
808
|
+
lines.push(t("bootstrap.verify.dialogueItem", {
|
|
809
|
+
path: item.sourcePath,
|
|
810
|
+
line: item.line,
|
|
811
|
+
snippet: item.snippet
|
|
812
|
+
}, locale));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return lines.join("\n");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function renderResumeCard(resume, locale = "en") {
|
|
819
|
+
const lines = [];
|
|
820
|
+
lines.push(t("bootstrap.resume.title", {}, locale));
|
|
821
|
+
if (resume.activeTasks.length === 0) {
|
|
822
|
+
lines.push(t("bootstrap.resume.noActive", {}, locale));
|
|
823
|
+
} else {
|
|
824
|
+
lines.push(t("bootstrap.resume.count", { count: resume.activeTasks.length, plural: resume.activeTasks.length === 1 ? "" : "s" }, locale));
|
|
825
|
+
for (const task of resume.activeTasks) {
|
|
826
|
+
const title = task.title && task.title.length > 0 ? task.title : t("common.untitled", {}, locale);
|
|
827
|
+
lines.push(t("bootstrap.resume.item", { id: task.id, title, status: task.status }, locale));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (resume.missingHandoff) {
|
|
831
|
+
lines.push(t("bootstrap.resume.missingHandoff", {}, locale));
|
|
832
|
+
}
|
|
833
|
+
if (resume.gitDrift.repeatedlyTouched.length > 0) {
|
|
834
|
+
const names = resume.gitDrift.repeatedlyTouched.map((r) => `${r.file} (×${r.commits})`).join(", ");
|
|
835
|
+
lines.push(t("bootstrap.resume.reTouch", { names }, locale));
|
|
836
|
+
lines.push(t("bootstrap.resume.reTouchNote", {}, locale));
|
|
837
|
+
}
|
|
838
|
+
if (resume.gitDrift.hasUncommittedChanges) {
|
|
839
|
+
lines.push(t("bootstrap.resume.uncommitted", {}, locale));
|
|
840
|
+
}
|
|
841
|
+
return lines.join("\n");
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function renderHarvestCard(harvest, locale = "en") {
|
|
845
|
+
const lines = [];
|
|
846
|
+
lines.push(t("bootstrap.harvest.title", {}, locale));
|
|
847
|
+
const dialogueCandidates = harvest.dialogueCandidates ?? [];
|
|
848
|
+
if (
|
|
849
|
+
harvest.confirmedLearnings.length === 0 &&
|
|
850
|
+
harvest.candidates.length === 0 &&
|
|
851
|
+
dialogueCandidates.length === 0
|
|
852
|
+
) {
|
|
853
|
+
lines.push(t("bootstrap.harvest.none1", {}, locale));
|
|
854
|
+
lines.push(t("bootstrap.harvest.none2", {}, locale));
|
|
855
|
+
return lines.join("\n");
|
|
856
|
+
}
|
|
857
|
+
for (const c of harvest.candidates) {
|
|
858
|
+
// Render the localized detail from the stable `kind` + `count`; fall back to the
|
|
859
|
+
// model's English `detail` if a kind has no message key (never an empty line).
|
|
860
|
+
const key = `bootstrap.harvest.detail.${c.kind}`;
|
|
861
|
+
const localizedDetail = t(key, { count: c.count, plural: c.count === 1 ? "" : "s" }, locale);
|
|
862
|
+
const detail = localizedDetail === key ? c.detail : localizedDetail;
|
|
863
|
+
lines.push(t("bootstrap.harvest.candidate", { detail }, locale));
|
|
864
|
+
}
|
|
865
|
+
if (harvest.confirmedLearnings.length > 0) {
|
|
866
|
+
lines.push(t("bootstrap.harvest.confirmedHead", {}, locale));
|
|
867
|
+
for (const l of harvest.confirmedLearnings) {
|
|
868
|
+
lines.push(t("bootstrap.harvest.confirmedItem", { type: l.type, content: l.content }, locale));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// The dialogue-sourced profile candidates: a SEPARATE block, labelled as coming
|
|
872
|
+
// from the chat and PROPOSED (nothing saved). Kept apart from the user's own
|
|
873
|
+
// confirmed learnings above so a chat-derived guess is never shown as a kept fact.
|
|
874
|
+
if (dialogueCandidates.length > 0) {
|
|
875
|
+
lines.push(t("bootstrap.harvest.dialogueHead", {}, locale));
|
|
876
|
+
for (const c of dialogueCandidates) {
|
|
877
|
+
lines.push(t("bootstrap.harvest.dialogueItem", {
|
|
878
|
+
count: c.count,
|
|
879
|
+
snippet: c.snippet
|
|
880
|
+
}, locale));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return lines.join("\n");
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Render the PROFILE-CLUES card. Leads the report with recognisable facts (a warm,
|
|
887
|
+
// factual opener), then a FIXED footer that states the honesty contract out loud:
|
|
888
|
+
// these are clues, not a conclusion, and the user's own ai will confirm the real
|
|
889
|
+
// work-style profile. Every line is a fact from the scan; none is a judgment.
|
|
890
|
+
function renderProfileCard(profile, locale = "en") {
|
|
891
|
+
const lines = [];
|
|
892
|
+
lines.push(t("bootstrap.profile.title", {}, locale));
|
|
893
|
+
|
|
894
|
+
const tools = Array.isArray(profile.detectedTools) ? profile.detectedTools : [];
|
|
895
|
+
if (tools.length > 0) {
|
|
896
|
+
lines.push(t("bootstrap.profile.tools", { tools: tools.join(", ") }, locale));
|
|
897
|
+
// More than one tool is a cross-tool FACT (two tools configured), surfaced as a
|
|
898
|
+
// collaboration SIGNAL — never "you like working across tools" as a verdict.
|
|
899
|
+
if (profile.multiTool) {
|
|
900
|
+
lines.push(t("bootstrap.profile.multiTool", {}, locale));
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
lines.push(t("bootstrap.profile.noTools", {}, locale));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const fileTypes = Array.isArray(profile.fileTypes) ? profile.fileTypes : [];
|
|
907
|
+
if (fileTypes.length > 0) {
|
|
908
|
+
const names = fileTypes.map((f) => `${f.ext} (×${f.count})`).join(", ");
|
|
909
|
+
lines.push(t("bootstrap.profile.fileTypes", { names }, locale));
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (profile.hasTestScript) {
|
|
913
|
+
lines.push(t("bootstrap.profile.testScript", {}, locale));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// The fixed footer — the load-bearing honesty line. Always present, never softened:
|
|
917
|
+
// "these are clues, not a conclusion; your ai will confirm the full profile."
|
|
918
|
+
lines.push(t("bootstrap.profile.footer", {}, locale));
|
|
919
|
+
return lines.join("\n");
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Render the ROLES-SUGGESTION card. Each hit shows the exact matched evidence (the
|
|
923
|
+
// task title or file path + the keyword) and the suggested role(s) with a plain-
|
|
924
|
+
// language simile, so the user sees WHY a role is suggested — a fact + a fixed
|
|
925
|
+
// mapping, never "this work is dangerous" as a verdict. No hit -> an honest "no
|
|
926
|
+
// high-risk keywords matched — a keyword scan only, not a low-risk verdict" line.
|
|
927
|
+
function renderRolesCard(roles, locale = "en") {
|
|
928
|
+
const lines = [];
|
|
929
|
+
lines.push(t("bootstrap.roles.title", {}, locale));
|
|
930
|
+
|
|
931
|
+
if (!roles.hasHighRisk) {
|
|
932
|
+
// Honest no-match: do not manufacture a suggestion. Say plainly that nothing
|
|
933
|
+
// high-risk was scanned and a single tool is enough until the user needs more.
|
|
934
|
+
lines.push(t("bootstrap.roles.none", {}, locale));
|
|
935
|
+
return lines.join("\n");
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
lines.push(t("bootstrap.roles.intro", { count: roles.hits.length, plural: roles.hits.length === 1 ? "" : "s" }, locale));
|
|
939
|
+
for (const hit of roles.hits) {
|
|
940
|
+
// The role names + their plain-language similes, joined. The simile lives in
|
|
941
|
+
// i18n (e.g. "red-team = someone whose whole job is to poke holes"), so the
|
|
942
|
+
// role label is never a bare term wall on the user's first read.
|
|
943
|
+
const roleText = hit.roles
|
|
944
|
+
.map((id) => t(`bootstrap.roles.role.${id}`, {}, locale))
|
|
945
|
+
.join(t("bootstrap.roles.roleJoin", {}, locale));
|
|
946
|
+
// The matched SUBJECT is shown verbatim (the task title or file path), then the
|
|
947
|
+
// keyword, then the suggestion — the full evidence chain, localized but faithful.
|
|
948
|
+
lines.push(t("bootstrap.roles.item", {
|
|
949
|
+
subject: hit.subject,
|
|
950
|
+
keyword: hit.keyword,
|
|
951
|
+
roles: roleText
|
|
952
|
+
}, locale));
|
|
953
|
+
}
|
|
954
|
+
return lines.join("\n");
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// The whole report as plain text. Leads with the honest framing, then the three
|
|
958
|
+
// Build the transparency block (red line #5) for a report that read a local dialogue
|
|
959
|
+
// or log export: which files were read (path + line count), how many snippets were
|
|
960
|
+
// flagged, the explicit "all local, nothing sent" promise, and a note for any file
|
|
961
|
+
// that was skipped (missing / unreadable / unsupported). Returns [] when no dialogue
|
|
962
|
+
// file was provided, so the default report stays byte-identical. Every snippet shown
|
|
963
|
+
// downstream is already redacted; this header only names files + counts.
|
|
964
|
+
function dialogueTransparencyLines(model, locale = "en") {
|
|
965
|
+
const d = model.dialogue;
|
|
966
|
+
if (!d || d.used !== true) {
|
|
967
|
+
// Even with no readable source, if the user NAMED files that were all skipped, be
|
|
968
|
+
// transparent about that (so a typo'd path is visible, not silently ignored).
|
|
969
|
+
if (d && Array.isArray(d.skipped) && d.skipped.length > 0) {
|
|
970
|
+
const out = [t("bootstrap.dialogue.skippedHead", {}, locale)];
|
|
971
|
+
for (const s of d.skipped) {
|
|
972
|
+
out.push(t(`bootstrap.dialogue.skipped.${s.reason}`, { path: s.path }, locale));
|
|
973
|
+
}
|
|
974
|
+
return out;
|
|
975
|
+
}
|
|
976
|
+
return [];
|
|
977
|
+
}
|
|
978
|
+
const out = [];
|
|
979
|
+
const fileList = d.sources.map((s) => `${s.path} (${s.lines})`).join(", ");
|
|
980
|
+
out.push(t("bootstrap.dialogue.head", {
|
|
981
|
+
count: d.sources.length,
|
|
982
|
+
plural: d.sources.length === 1 ? "" : "s",
|
|
983
|
+
files: fileList,
|
|
984
|
+
snippets: d.snippetCount
|
|
985
|
+
}, locale));
|
|
986
|
+
out.push(t("bootstrap.dialogue.localPromise", {}, locale));
|
|
987
|
+
if (Array.isArray(d.skipped) && d.skipped.length > 0) {
|
|
988
|
+
for (const s of d.skipped) {
|
|
989
|
+
out.push(t(`bootstrap.dialogue.skipped.${s.reason}`, { path: s.path }, locale));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return out;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// cards, then a single concrete next step. Used for the non-JSON output.
|
|
996
|
+
export function renderBootstrapReport(model, locale = "en") {
|
|
997
|
+
const lines = [];
|
|
998
|
+
lines.push(t("bootstrap.report.title", {}, locale));
|
|
999
|
+
lines.push(t("bootstrap.report.scanned", { repoRoot: model.scan.repoRoot }, locale));
|
|
1000
|
+
lines.push(t("bootstrap.report.readonly", {}, locale));
|
|
1001
|
+
// Transparency (red line #5): when a local dialogue/log export was read, state at
|
|
1002
|
+
// the TOP exactly which files, how many flagged snippets, and that it stayed local.
|
|
1003
|
+
for (const line of dialogueTransparencyLines(model, locale)) lines.push(line);
|
|
1004
|
+
lines.push("");
|
|
1005
|
+
|
|
1006
|
+
if (!model.hasOwnData && !model.dialogueUsed) {
|
|
1007
|
+
// Seed-honesty: no data of the user's own AND no dialogue handed over. Do NOT
|
|
1008
|
+
// dress up the shipped example. (If a dialogue WAS read, it is the user's own data,
|
|
1009
|
+
// so we fall through and render the cards with the dialogue findings.)
|
|
1010
|
+
lines.push(t("bootstrap.empty.line1", {}, locale));
|
|
1011
|
+
lines.push(t("bootstrap.empty.line2", {}, locale));
|
|
1012
|
+
lines.push(t("bootstrap.empty.step1", {}, locale));
|
|
1013
|
+
lines.push(t("bootstrap.empty.step2", {}, locale));
|
|
1014
|
+
lines.push("");
|
|
1015
|
+
lines.push(t("bootstrap.empty.note", {}, locale));
|
|
1016
|
+
return lines.join("\n");
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Card order (per the product brief): profile leads with recognisable facts so the
|
|
1020
|
+
// first screen earns goodwill, then VERIFY (the most urgent honesty signal), RESUME,
|
|
1021
|
+
// the roles suggestion (high-risk -> bring help), and HARVEST. The two new cards are
|
|
1022
|
+
// DETERMINISTIC fact lists; VERIFY + roles remain the load-bearing content.
|
|
1023
|
+
lines.push(renderProfileCard(model.cards.profile, locale));
|
|
1024
|
+
lines.push("");
|
|
1025
|
+
lines.push(renderVerifyCard(model.cards.verify, locale));
|
|
1026
|
+
lines.push("");
|
|
1027
|
+
lines.push(renderResumeCard(model.cards.resume, locale));
|
|
1028
|
+
lines.push("");
|
|
1029
|
+
lines.push(renderRolesCard(model.cards.roles, locale));
|
|
1030
|
+
lines.push("");
|
|
1031
|
+
lines.push(renderHarvestCard(model.cards.harvest, locale));
|
|
1032
|
+
lines.push("");
|
|
1033
|
+
// One concrete next step, chosen from the real model — and it points at an
|
|
1034
|
+
// EXISTING audited command with the real id filled in, so the user can act
|
|
1035
|
+
// without translating the advice into a command (and without inventing a flag).
|
|
1036
|
+
for (const line of bootstrapNextStepLines(model, locale)) lines.push(line);
|
|
1037
|
+
return lines.join("\n");
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Build the "Next:" block for the bootstrap report. Returns 1-2 lines: a plain
|
|
1041
|
+
// sentence plus, when applicable, a real copy-pasteable command with the actual id
|
|
1042
|
+
// filled in. Ordered VERIFY (most urgent) -> RESUME -> HARVEST -> all-clear. Every
|
|
1043
|
+
// command is one the CLI already ships and audits (receipt accept / receipt create /
|
|
1044
|
+
// run exec / handoff create / learning confirm) — bootstrap stays report-only and
|
|
1045
|
+
// never invents a write path (no --save-safe; the only sanctioned write is the
|
|
1046
|
+
// existing learning add/confirm + receipt flow the user runs themselves).
|
|
1047
|
+
export function bootstrapNextStepLines(model, locale = "en") {
|
|
1048
|
+
// The PRIMARY next step keeps its original priority ladder VERIFY -> RESUME ->
|
|
1049
|
+
// HARVEST -> all-clear (VERIFY is the most urgent honesty signal, so it stays the
|
|
1050
|
+
// headline action). The roles HINT is APPENDED after it (not in place of it) when
|
|
1051
|
+
// high-risk roles were suggested, so the user is told to bring help without burying
|
|
1052
|
+
// the verify action. A roles hint is advisory — it points at an EXISTING role
|
|
1053
|
+
// package to read, never a write path.
|
|
1054
|
+
const primary = bootstrapPrimaryNextStep(model, locale);
|
|
1055
|
+
|
|
1056
|
+
// Append the high-risk roles hint, if any. The roles card already de-duped + capped
|
|
1057
|
+
// the suggestions; we surface their plain-language names so the user knows who to
|
|
1058
|
+
// bring — the spec's "if high-risk roles were suggested, fold them into Next".
|
|
1059
|
+
const roles = model.cards.roles;
|
|
1060
|
+
if (roles && roles.hasHighRisk && Array.isArray(roles.roles) && roles.roles.length > 0) {
|
|
1061
|
+
const roleText = roles.roles
|
|
1062
|
+
.map((r) => t(`bootstrap.roles.role.${r.id}`, {}, locale))
|
|
1063
|
+
.join(t("bootstrap.roles.roleJoin", {}, locale));
|
|
1064
|
+
return [...primary, t("bootstrap.next.roles.text", { roles: roleText }, locale)];
|
|
1065
|
+
}
|
|
1066
|
+
return primary;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// The primary next-step lines (the original VERIFY -> RESUME -> HARVEST -> all-clear
|
|
1070
|
+
// ladder). Split out so bootstrapNextStepLines can append the roles hint after it
|
|
1071
|
+
// without duplicating the ladder. Returns 1-2 lines: a plain sentence + an optional
|
|
1072
|
+
// real copy-pasteable command. Every command is one the CLI already ships and audits.
|
|
1073
|
+
function bootstrapPrimaryNextStep(model, locale = "en") {
|
|
1074
|
+
// (1) VERIFY (ledger): something is claimed done but cannot be trusted. Point at the
|
|
1075
|
+
// real command that closes the specific gap, with the offending id filled in. We
|
|
1076
|
+
// deliberately pick the first LEDGER item (not a dialogue one) for the receipt-
|
|
1077
|
+
// based commands, since only a ledger item carries a taskId/receipt to act on.
|
|
1078
|
+
const verifyItem = model.cards.verify.find((v) => v.source !== "dialogue");
|
|
1079
|
+
if (verifyItem) {
|
|
1080
|
+
// A receipt that is merely PENDING (a pass_with_risk awaiting sign-off) closes
|
|
1081
|
+
// with an owner acceptance — the lightest real action, so suggest it directly.
|
|
1082
|
+
if (verifyItem.receipt && verifyItem.receipt.status === "pending") {
|
|
1083
|
+
return [
|
|
1084
|
+
t("bootstrap.next.pending.text", { receiptId: verifyItem.receipt.id, taskId: verifyItem.taskId }, locale),
|
|
1085
|
+
t("bootstrap.next.pending.cmd", { receiptId: verifyItem.receipt.id }, locale)
|
|
1086
|
+
];
|
|
1087
|
+
}
|
|
1088
|
+
// A self-declared cross-family pass is accepted but UNVERIFIED: the honest way to
|
|
1089
|
+
// strengthen it is a reviewer rerun reconciled to a recorded run exec (the L4 path).
|
|
1090
|
+
if (verifyItem.reason === "self_declared_cross_family") {
|
|
1091
|
+
return [
|
|
1092
|
+
t("bootstrap.next.selfCross.text", { receiptId: verifyItem.receipt ? verifyItem.receipt.id : "(none)", taskId: verifyItem.taskId }, locale),
|
|
1093
|
+
t("bootstrap.next.selfCross.cmd", { taskId: verifyItem.taskId }, locale)
|
|
1094
|
+
];
|
|
1095
|
+
}
|
|
1096
|
+
// Author-marked done with no accepted review behind it: file a real receipt
|
|
1097
|
+
// (after the evidence exists) so the "done" is backed by the guard.
|
|
1098
|
+
return [
|
|
1099
|
+
t("bootstrap.next.authorDone.text", { taskId: verifyItem.taskId }, locale),
|
|
1100
|
+
t("bootstrap.next.authorDone.cmd", { taskId: verifyItem.taskId }, locale)
|
|
1101
|
+
];
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// (1b) VERIFY (dialogue): no ledger item, but the chat export claims a "done" the
|
|
1105
|
+
// ledger does not back. The honest next step is to turn that claim into a real,
|
|
1106
|
+
// tracked task + recorded run (so the "done" stops being just words). Points at
|
|
1107
|
+
// `task create`, an existing audited command — never invents a write path.
|
|
1108
|
+
const dialogueVerify = model.cards.verify.find((v) => v.source === "dialogue");
|
|
1109
|
+
if (dialogueVerify) {
|
|
1110
|
+
return [
|
|
1111
|
+
t("bootstrap.next.dialogueClaim.text", {}, locale),
|
|
1112
|
+
t("bootstrap.next.dialogueClaim.cmd", {}, locale)
|
|
1113
|
+
];
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// (2) RESUME: work is in flight but there is no handoff draft to resume from.
|
|
1117
|
+
if (model.cards.resume.missingHandoff) {
|
|
1118
|
+
return [
|
|
1119
|
+
t("bootstrap.next.missingHandoff.text", {}, locale),
|
|
1120
|
+
t("bootstrap.next.missingHandoff.cmd", {}, locale)
|
|
1121
|
+
];
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// (3) HARVEST: a proposed lesson of your own is waiting to be kept — confirming it
|
|
1125
|
+
// graduates it into your profile (the real, audited write path).
|
|
1126
|
+
const proposed = model.cards.harvest.proposedLearnings && model.cards.harvest.proposedLearnings[0];
|
|
1127
|
+
if (proposed) {
|
|
1128
|
+
return [
|
|
1129
|
+
t("bootstrap.next.keepLesson.text", { id: proposed.id }, locale),
|
|
1130
|
+
t("bootstrap.next.keepLesson.cmd", { id: proposed.id }, locale)
|
|
1131
|
+
];
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// (4) Nothing outstanding.
|
|
1135
|
+
return [
|
|
1136
|
+
t("bootstrap.next.allClear.text", {}, locale)
|
|
1137
|
+
];
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// --- D. Consent preview (printed before the scan unless --yes) --------------
|
|
1141
|
+
//
|
|
1142
|
+
// bootstrap reads local files + runs read-only git. Before doing so it prints
|
|
1143
|
+
// EXACTLY what it will read and confirms it stays local, then (without --yes) stops
|
|
1144
|
+
// and asks the user to re-run with --yes. The CLI is non-interactive, so "consent"
|
|
1145
|
+
// is an explicit re-run, not a y/n prompt — but the scope is shown first either way.
|
|
1146
|
+
export function renderConsentPreview(repoRoot, locale = "en", dialogueSources = []) {
|
|
1147
|
+
const lines = [
|
|
1148
|
+
t("bootstrap.consent.head", {}, locale),
|
|
1149
|
+
t("bootstrap.consent.repo", { repoRoot }, locale),
|
|
1150
|
+
t("bootstrap.consent.git", {}, locale),
|
|
1151
|
+
t("bootstrap.consent.ledger", {}, locale),
|
|
1152
|
+
t("bootstrap.consent.ai", {}, locale)
|
|
1153
|
+
];
|
|
1154
|
+
// The dialogue/log connectors are a HIGH-PRIVACY source that is OFF unless the user
|
|
1155
|
+
// EXPLICITLY named a file. Only when they did do we list those files in the consent
|
|
1156
|
+
// scope (so the preview names exactly what extra will be read); otherwise the scope
|
|
1157
|
+
// is identical to before — the connector defaults to "not read".
|
|
1158
|
+
if (Array.isArray(dialogueSources) && dialogueSources.length > 0) {
|
|
1159
|
+
lines.push(t("bootstrap.consent.dialogue", { files: dialogueSources.join(", ") }, locale));
|
|
1160
|
+
}
|
|
1161
|
+
lines.push("");
|
|
1162
|
+
lines.push(t("bootstrap.consent.promise", {}, locale));
|
|
1163
|
+
lines.push(t("bootstrap.consent.rerun", {}, locale));
|
|
1164
|
+
lines.push(t("bootstrap.consent.cmd", {}, locale));
|
|
1165
|
+
return lines.join("\n");
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// TODO (next sub-batch / Owner's call — deliberately NOT here):
|
|
1169
|
+
// - the EXTERNAL-model half of the semantic scan (`--send-to-model`): send the
|
|
1170
|
+
// redacted snippets to a model for a richer read. Kept out HERE: this batch is the
|
|
1171
|
+
// LOCAL half only — red line #1 is deterministic + no-network, so the model pass is
|
|
1172
|
+
// a separate, explicitly-consented step. redactSnippet (dialogue.js) is already
|
|
1173
|
+
// shared so that path reuses the exact same redaction before anything is sent.
|
|
1174
|
+
// - a save / write-back flow (--save-safe) that promotes a HARVEST candidate into
|
|
1175
|
+
// the profile. This batch is report-only and writes NOTHING; the proposed/confirmed
|
|
1176
|
+
// buffer (learning add/confirm) is the only sanctioned write path today.
|
|
1177
|
+
// - a GUI / richer rendering.
|
|
1178
|
+
//
|
|
1179
|
+
// DONE in this batch (the local half of semantic scanning, see dialogue.js):
|
|
1180
|
+
// - opt-in local connectors `--dialogue` / `--logs` (read only files the user names).
|
|
1181
|
+
// - deterministic completion-claim extraction cross-referenced against the ledger,
|
|
1182
|
+
// surfaced as VERIFY findings "claimed in dialogue · not verified" (never done).
|
|
1183
|
+
// - repeated-correction -> HARVEST *profile* candidate (proposed; nothing written).
|
|
1184
|
+
// - per-snippet redaction (redactSnippet) before anything is shown or recorded.
|