@magic-spells/constellation 0.2.2 → 0.3.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/README.md +13 -6
- package/constellation/agent/AGENT-GUIDANCE.md +22 -0
- package/constellation/component/COMPONENT-EDITABLE.md +9 -0
- package/constellation/component/COMPONENT-STATUS-PILL.md +9 -0
- package/constellation/component/COMPONENT-SYNC-PILL.md +9 -0
- package/constellation/diagram/DIAGRAM-ARCHITECTURE.md +28 -0
- package/constellation/doc/DOC-CARD-TYPES.md +36 -0
- package/constellation/doc/DOC-CHANGE-TRACKING.md +26 -0
- package/constellation/doc/DOC-CONNECTED-REPOS.md +25 -0
- package/constellation/doc/DOC-DIAGRAMS.md +22 -0
- package/constellation/doc/DOC-FILE-FORMAT.md +54 -0
- package/constellation/doc/DOC-LINT-CODES.md +35 -0
- package/constellation/doc/DOC-MCP-SERVER.md +59 -0
- package/constellation/doc/DOC-MCP-UPGRADES.md +56 -0
- package/constellation/external/EXTERNAL-AJV.md +7 -0
- package/constellation/external/EXTERNAL-GIT.md +7 -0
- package/constellation/external/EXTERNAL-GRAY-MATTER.md +7 -0
- package/constellation/external/EXTERNAL-MCP-SDK.md +7 -0
- package/constellation/external/EXTERNAL-ZOD.md +7 -0
- package/constellation/file/FILE-CLI.md +9 -0
- package/constellation/file/FILE-CODE.md +9 -0
- package/constellation/file/FILE-EXTRACT.md +9 -0
- package/constellation/file/FILE-GIT.md +9 -0
- package/constellation/file/FILE-HANDLES.md +9 -0
- package/constellation/file/FILE-INDEXER.md +11 -0
- package/constellation/file/FILE-LINT.md +9 -0
- package/constellation/file/FILE-MCP-SEARCH.md +12 -0
- package/constellation/file/FILE-MCP-SERVER.md +9 -0
- package/constellation/file/FILE-PARSE.md +9 -0
- package/constellation/file/FILE-RENAME.md +18 -0
- package/constellation/file/FILE-REPOS.md +9 -0
- package/constellation/file/FILE-RESOLVE.md +9 -0
- package/constellation/file/FILE-SCAFFOLD.md +12 -0
- package/constellation/file/FILE-SERVE.md +9 -0
- package/constellation/file/FILE-SYNC.md +9 -0
- package/constellation/file/FILE-TYPES.md +9 -0
- package/constellation/file/FILE-VALIDATE.md +11 -0
- package/constellation/file/FILE-WRITER.md +11 -0
- package/constellation/flow/FLOW-DRIFT-DETECT.md +11 -0
- package/constellation/flow/FLOW-LINT-PIPELINE.md +14 -0
- package/constellation/flow/FLOW-SYNC-PLAN.md +13 -0
- package/constellation/page/PAGE-VIEWER-CARD.md +9 -0
- package/constellation/page/PAGE-VIEWER-HOME.md +9 -0
- package/constellation/plan.md +59 -0
- package/constellation/test/TEST-SUITE.md +9 -0
- package/dist/cli/index.js +37 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/code.d.ts +4 -2
- package/dist/core/code.js +22 -7
- package/dist/core/code.js.map +1 -1
- package/dist/core/git.d.ts +2 -0
- package/dist/core/git.js +6 -0
- package/dist/core/git.js.map +1 -1
- package/dist/core/rename.d.ts +24 -0
- package/dist/core/rename.js +72 -0
- package/dist/core/rename.js.map +1 -0
- package/dist/core/repos.d.ts +2 -0
- package/dist/core/repos.js +5 -1
- package/dist/core/repos.js.map +1 -1
- package/dist/core/sync.d.ts +2 -0
- package/dist/core/sync.js +3 -1
- package/dist/core/sync.js.map +1 -1
- package/dist/core/validate.js +14 -1
- package/dist/core/validate.js.map +1 -1
- package/dist/core/writer.d.ts +29 -0
- package/dist/core/writer.js +98 -3
- package/dist/core/writer.js.map +1 -1
- package/dist/mcp/search.d.ts +2 -2
- package/dist/mcp/search.js +27 -5
- package/dist/mcp/search.js.map +1 -1
- package/dist/mcp/server.js +300 -99
- package/dist/mcp/server.js.map +1 -1
- package/dist/serve/server.js +6 -7
- package/dist/serve/server.js.map +1 -1
- package/package.json +2 -2
- package/skill/SKILL.md +11 -5
- package/skill/methodology.md +6 -2
- package/viewer/dist/assets/{arc-YuzGWkw8.js → arc-ByV7p1fN.js} +1 -1
- package/viewer/dist/assets/architecture-7EHR7CIX-FCXXAu3O.js +1 -0
- package/viewer/dist/assets/{architectureDiagram-3BPJPVTR-BgksqMlF.js → architectureDiagram-3BPJPVTR-CMawC2nt.js} +1 -1
- package/viewer/dist/assets/{blockDiagram-GPEHLZMM-Bc6JILFK.js → blockDiagram-GPEHLZMM-Bp0MKYXV.js} +1 -1
- package/viewer/dist/assets/{c4Diagram-AAUBKEIU-CRVt42df.js → c4Diagram-AAUBKEIU-BLCX9gmN.js} +1 -1
- package/viewer/dist/assets/channel-GJMBi5UK.js +1 -0
- package/viewer/dist/assets/{chunk-2J33WTMH-CpuwKeX6.js → chunk-2J33WTMH-C6iOidst.js} +1 -1
- package/viewer/dist/assets/{chunk-3OPIFGDE-BG_ijJhE.js → chunk-3OPIFGDE-B9xItVw4.js} +1 -1
- package/viewer/dist/assets/{chunk-4BX2VUAB-Bgz1tnDP.js → chunk-4BX2VUAB-Bl0HWb9B.js} +1 -1
- package/viewer/dist/assets/{chunk-55IACEB6-NdWURd2K.js → chunk-55IACEB6-BYilo1WJ.js} +1 -1
- package/viewer/dist/assets/{chunk-5ZQYHXKU-wzW5x_7z.js → chunk-5ZQYHXKU-DOCnP8P8.js} +1 -1
- package/viewer/dist/assets/{chunk-727SXJPM-BxKeJXYf.js → chunk-727SXJPM-D9YKAzha.js} +1 -1
- package/viewer/dist/assets/{chunk-AQP2D5EJ-gPm6xhPX.js → chunk-AQP2D5EJ-B1KE4h_b.js} +1 -1
- package/viewer/dist/assets/{chunk-BSJP7CBP-C30ZXEUD.js → chunk-BSJP7CBP-DA3pfXCw.js} +1 -1
- package/viewer/dist/assets/{chunk-CSCIHK7Q-C9h7yfl1.js → chunk-CSCIHK7Q-Bv0Fa0Zn.js} +6 -6
- package/viewer/dist/assets/{chunk-FMBD7UC4-BcSnkYc3.js → chunk-FMBD7UC4-k-EzzpU2.js} +1 -1
- package/viewer/dist/assets/{chunk-KSCS5N6A-rxSD5-MD.js → chunk-KSCS5N6A-CUTkv0jR.js} +1 -1
- package/viewer/dist/assets/{chunk-L5ZTLDWV-DGwFsVWy.js → chunk-L5ZTLDWV-C_SwE6oB.js} +1 -1
- package/viewer/dist/assets/{chunk-LZXEDZCA-BY3sTuvt.js → chunk-LZXEDZCA-DtwqVIDt.js} +2 -2
- package/viewer/dist/assets/{chunk-ND2GUHAM-PqmKwdzq.js → chunk-ND2GUHAM-JJiAqLG2.js} +1 -1
- package/viewer/dist/assets/{chunk-NZK2D7GU-BAZbzxhl.js → chunk-NZK2D7GU-BQ8Dn7FA.js} +1 -1
- package/viewer/dist/assets/{chunk-O5CBEL6O-SvZfZ-0j.js → chunk-O5CBEL6O-BRpqOJ1I.js} +1 -1
- package/viewer/dist/assets/chunk-QZHKN3VN-DA8qsAVF.js +1 -0
- package/viewer/dist/assets/chunk-WU5MYG2G-Dp8LMm21.js +1 -0
- package/viewer/dist/assets/{chunk-XPW4576I-1OXPbRQX.js → chunk-XPW4576I-C1NcqPJd.js} +1 -1
- package/viewer/dist/assets/classDiagram-4FO5ZUOK-wnkp0LBB.js +1 -0
- package/viewer/dist/assets/classDiagram-v2-Q7XG4LA2-wnkp0LBB.js +1 -0
- package/viewer/dist/assets/{cose-bilkent-S5V4N54A-DtCvLpah.js → cose-bilkent-S5V4N54A-BsJFb39B.js} +1 -1
- package/viewer/dist/assets/{dagre-BM42HDAG-B55Ilz10.js → dagre-BM42HDAG-CRPovG4Q.js} +1 -1
- package/viewer/dist/assets/{diagram-2AECGRRQ-ClqSMarp.js → diagram-2AECGRRQ-D6GCKn1u.js} +1 -1
- package/viewer/dist/assets/{diagram-5GNKFQAL-Dx74UdxY.js → diagram-5GNKFQAL-CoHqKdf3.js} +1 -1
- package/viewer/dist/assets/{diagram-KO2AKTUF-Vj0buH8e.js → diagram-KO2AKTUF-BEkMb40X.js} +1 -1
- package/viewer/dist/assets/{diagram-LMA3HP47-DpEm8RZd.js → diagram-LMA3HP47-B6mfW9S2.js} +1 -1
- package/viewer/dist/assets/{diagram-OG6HWLK6-Crl3BdwC.js → diagram-OG6HWLK6-OBBkuJvR.js} +1 -1
- package/viewer/dist/assets/{dist-C2uHOEFB.js → dist-C4BxOpXa.js} +1 -1
- package/viewer/dist/assets/{erDiagram-TEJ5UH35-B-Mf-EHP.js → erDiagram-TEJ5UH35-C2i3TB2-.js} +1 -1
- package/viewer/dist/assets/eventmodeling-FCH6USID-CCZDiBK0.js +1 -0
- package/viewer/dist/assets/{flowDiagram-I6XJVG4X-B9stChWN.js → flowDiagram-I6XJVG4X-SreJFib5.js} +1 -1
- package/viewer/dist/assets/{ganttDiagram-6RSMTGT7-DzWlclwM.js → ganttDiagram-6RSMTGT7-DO7GLSdw.js} +1 -1
- package/viewer/dist/assets/{gitGraph-WXDBUCRP-CdVI3GTp.js → gitGraph-WXDBUCRP-Cgwn_Q5_.js} +1 -1
- package/viewer/dist/assets/{gitGraphDiagram-PVQCEYII-Dn9yRc1N.js → gitGraphDiagram-PVQCEYII-DLwaMyDa.js} +1 -1
- package/viewer/dist/assets/{index-BsGRgBOD.js → index-COz6c_JS.js} +2 -2
- package/viewer/dist/assets/{info-J43DQDTF-CXV3Mm9T.js → info-J43DQDTF-Ci-BiM87.js} +1 -1
- package/viewer/dist/assets/{infoDiagram-5YYISTIA-zIUka_J2.js → infoDiagram-5YYISTIA-Bip5dlKM.js} +1 -1
- package/viewer/dist/assets/{ishikawaDiagram-YF4QCWOH-DsZwNuE3.js → ishikawaDiagram-YF4QCWOH-VXV_9uEH.js} +1 -1
- package/viewer/dist/assets/{journeyDiagram-JHISSGLW-BhSTSB2s.js → journeyDiagram-JHISSGLW-DQz-8gAS.js} +1 -1
- package/viewer/dist/assets/{kanban-definition-UN3LZRKU-sCAldj86.js → kanban-definition-UN3LZRKU-BTWpEcf0.js} +1 -1
- package/viewer/dist/assets/{line-D7b3fopb.js → line-BnRWAabF.js} +1 -1
- package/viewer/dist/assets/{linear-BWlvVc_d.js → linear-BR6v_8SI.js} +1 -1
- package/viewer/dist/assets/{mermaid-parser.core-BdEYBvaj.js → mermaid-parser.core-CSsnrQhR.js} +2 -2
- package/viewer/dist/assets/{mermaid.core-C9bGWeJU.js → mermaid.core-I1kLnpG2.js} +3 -3
- package/viewer/dist/assets/{mindmap-definition-RKZ34NQL-DbTvvku-.js → mindmap-definition-RKZ34NQL-Cq66Ilr1.js} +1 -1
- package/viewer/dist/assets/{packet-YPE3B663-DRfLruyq.js → packet-YPE3B663-BjSCEGVd.js} +1 -1
- package/viewer/dist/assets/{pie-LRSECV5Y-47xXMkK-.js → pie-LRSECV5Y-Di72yxoZ.js} +1 -1
- package/viewer/dist/assets/{pieDiagram-4H26LBE5-Ckno9E1M.js → pieDiagram-4H26LBE5-TmsfKHOO.js} +1 -1
- package/viewer/dist/assets/{quadrantDiagram-W4KKPZXB-XB5kJ_2c.js → quadrantDiagram-W4KKPZXB-Dp9iSvCk.js} +1 -1
- package/viewer/dist/assets/{radar-GUYGQ44K-CLwIH5Zr.js → radar-GUYGQ44K-CZeWC3Jm.js} +1 -1
- package/viewer/dist/assets/{requirementDiagram-4Y6WPE33-D3fApL0X.js → requirementDiagram-4Y6WPE33-lbTbiw59.js} +1 -1
- package/viewer/dist/assets/{sankeyDiagram-5OEKKPKP-6UdaRhuZ.js → sankeyDiagram-5OEKKPKP-DqmoGDJk.js} +1 -1
- package/viewer/dist/assets/{sequenceDiagram-3UESZ5HK-ClIrrs0w.js → sequenceDiagram-3UESZ5HK-B6_EiWF6.js} +1 -1
- package/viewer/dist/assets/{src-BPb55SrS.js → src-Dz3SjXJY.js} +1 -1
- package/viewer/dist/assets/{stateDiagram-AJRCARHV-CCsrcv5R.js → stateDiagram-AJRCARHV-Z6QRASCx.js} +1 -1
- package/viewer/dist/assets/stateDiagram-v2-BHNVJYJU-BePyUY0u.js +1 -0
- package/viewer/dist/assets/{timeline-definition-PNZ67QCA-BkiWQlVI.js → timeline-definition-PNZ67QCA-XuUZAYAP.js} +1 -1
- package/viewer/dist/assets/{treeView-BLDUP644-m1mGpAE8.js → treeView-BLDUP644-DuGsPB97.js} +1 -1
- package/viewer/dist/assets/{treemap-LRROVOQU-Bapvwi0Q.js → treemap-LRROVOQU-CSJx8IZ0.js} +1 -1
- package/viewer/dist/assets/{vennDiagram-CIIHVFJN-BlH71Nmx.js → vennDiagram-CIIHVFJN-BnebDryc.js} +1 -1
- package/viewer/dist/assets/{wardley-L42UT6IY-CPRSoKgV.js → wardley-L42UT6IY-DqqoXPSq.js} +1 -1
- package/viewer/dist/assets/{wardleyDiagram-YWT4CUSO-C-ptFJ9t.js → wardleyDiagram-YWT4CUSO-Bchy871K.js} +1 -1
- package/viewer/dist/assets/{xychartDiagram-2RQKCTM6-C4rfeH4W.js → xychartDiagram-2RQKCTM6-Bl_zzGZm.js} +1 -1
- package/viewer/dist/index.html +1 -1
- package/docs/001-file-format.md +0 -265
- package/docs/002-mcp.md +0 -186
- package/docs/003-mcp-upgrade-plan.md +0 -138
- package/viewer/dist/assets/architecture-7EHR7CIX-BAboPvK1.js +0 -1
- package/viewer/dist/assets/channel-D8YnBgqu.js +0 -1
- package/viewer/dist/assets/chunk-QZHKN3VN-ClYnv9g5.js +0 -1
- package/viewer/dist/assets/chunk-WU5MYG2G-CvL2Yleq.js +0 -1
- package/viewer/dist/assets/classDiagram-4FO5ZUOK-B8YUd1jx.js +0 -1
- package/viewer/dist/assets/classDiagram-v2-Q7XG4LA2-B8YUd1jx.js +0 -1
- package/viewer/dist/assets/eventmodeling-FCH6USID-CZ5gxAxG.js +0 -1
- package/viewer/dist/assets/stateDiagram-v2-BHNVJYJU-C1phrPNi.js +0 -1
package/dist/mcp/server.js
CHANGED
|
@@ -9,12 +9,13 @@ import { loadPlan } from '../core/indexer.js';
|
|
|
9
9
|
import { lintPlan } from '../core/lint.js';
|
|
10
10
|
import { resolvePlanDir } from '../core/resolve.js';
|
|
11
11
|
import { TYPE_NAMES } from '../core/types.js';
|
|
12
|
-
import { changedFilesSince, diffPlan, headSha, planDirty, planLog, readSyncPoint, writeSyncPoint, } from '../core/git.js';
|
|
12
|
+
import { changedFilesSince, diffPlan, headSha, planDirty, planLog, readSyncPoint, resolveCommit, writeSyncPoint, } from '../core/git.js';
|
|
13
13
|
import { computeSyncStatus } from '../core/sync.js';
|
|
14
14
|
import { boundPathsForCard, resolveCodeForCard } from '../core/code.js';
|
|
15
15
|
import { searchCards } from './search.js';
|
|
16
|
-
import { applyCardPatch, bodyHeadingTexts, createCardFile, deepMerge, relPathForHandle, replaceBodySection, reservedFieldKeys,
|
|
17
|
-
import {
|
|
16
|
+
import { applyCardPatch, bodyHeadingTexts, createCardFile, deepMerge, mutateCardFile, relPathForHandle, replaceBodySection, reservedFieldKeys, withAppendedNote, } from '../core/writer.js';
|
|
17
|
+
import { renameCard, RenameCardError } from '../core/rename.js';
|
|
18
|
+
import { connectedRepoToFm, connectedReposFromFrontmatter, listConnectedRepos, readConnectedRepos, removeConnectedRepoEntry, resolveConnectedRepo, upsertConnectedRepo, } from '../core/repos.js';
|
|
18
19
|
const INSTRUCTIONS = `# Constellation MCP
|
|
19
20
|
|
|
20
21
|
Constellation is this project's durable, cross-session memory for AI agents — treat it as
|
|
@@ -40,9 +41,9 @@ Retrieval is hydrated: get_card / search / traverse can return connected cards w
|
|
|
40
41
|
their FULL frontmatter and body in one call (connected: "full"). Use that when you are
|
|
41
42
|
about to work on an area; use "summary" for orientation. get_card can also hand back the
|
|
42
43
|
CODE a card is bound to — code: "paths" returns the resolved file paths of its connected
|
|
43
|
-
FILE cards (path:) plus its own code_refs; code: "direct" attaches their contents (
|
|
44
|
-
binaries/lockfiles/generated skipped) so a background coder starts from
|
|
45
|
-
code in one call. assemble turns a delta (or a handle set) into a work package: the changed
|
|
44
|
+
FILE cards (path:) plus its own code_refs; code: "direct" attaches their contents (over-cap
|
|
45
|
+
files truncated, binaries/lockfiles/generated skipped) so a background coder starts from
|
|
46
|
+
intent + current code in one call. assemble turns a delta (or a handle set) into a work package: the changed
|
|
46
47
|
cards + their neighborhood (full) + bound code + a heuristic build order + FILE-DISJOINT
|
|
47
48
|
units you can fan out one sub-agent per, with no two touching the same file.
|
|
48
49
|
|
|
@@ -52,7 +53,11 @@ Body-only updates never reformat frontmatter. Prefer SMALL, cheap writes over re
|
|
|
52
53
|
whole card — make the honest update the easy one: append_note adds an append-only typed note
|
|
53
54
|
(decision / gotcha / state / deviation / verified) with no full-body rewrite; edit_section
|
|
54
55
|
replaces a single ## section in place. Reach for these to record a correction the moment you
|
|
55
|
-
learn it, so cards stay true instead of drifting.
|
|
56
|
+
learn it, so cards stay true instead of drifting. Notes are retrievable memory: search
|
|
57
|
+
matches note text, and list_notes lists them across cards by kind — every gotcha or decision
|
|
58
|
+
in one call. To rename a handle, use rename_card: it moves the file and rewrites every
|
|
59
|
+
reference plan-wide (connections, frontmatter values, [[links]], mermaid node IDs) as whole
|
|
60
|
+
tokens — never delete-and-recreate to rename.
|
|
56
61
|
|
|
57
62
|
describe_type is the type reference, served by this server: call it with no args for the
|
|
58
63
|
catalog of all 17 card types, or with a type (e.g. describe_type PAGE) for that type's
|
|
@@ -94,8 +99,8 @@ changes as the proposal; on approval, bring the CODE up to match via the sync lo
|
|
|
94
99
|
FINISH by reconciling — re-read the touched cards against the code, run check_integrity so
|
|
95
100
|
no affected card is left an orphan and every connection is set, bump status (planned →
|
|
96
101
|
building → built → verified), commit, and set_sync_point. In plan mode the write tools are
|
|
97
|
-
unavailable by design (the read tools — get_card, list_cards, search, traverse,
|
|
98
|
-
describe_type, check_integrity, diff_plan, plan_log, stale_report, check_sync,
|
|
102
|
+
unavailable by design (the read tools — get_card, list_cards, list_notes, search, traverse,
|
|
103
|
+
assemble, describe_type, check_integrity, diff_plan, plan_log, stale_report, check_sync,
|
|
99
104
|
list_connected_repos — are marked read-only and stay available), so spend plan mode READING:
|
|
100
105
|
pull in as much of the relevant plan as you can
|
|
101
106
|
(traverse from the entry points, connected: "full") to build a strong model of the project
|
|
@@ -106,7 +111,11 @@ For migrations or large scaffolds, use create_cards and add_connections (batched
|
|
|
106
111
|
lint pass) instead of many single calls — connections between cards in the same batch
|
|
107
112
|
resolve without transient "does not resolve" errors. A card is created even when issues
|
|
108
113
|
are returned (issues are lint state, not failure). check_integrity reports orphans
|
|
109
|
-
(zero-connection cards), and list_cards connected:false lists them.
|
|
114
|
+
(zero-connection cards), and list_cards connected:false lists them. The BACKLOG view —
|
|
115
|
+
everything not yet built — is list_cards status: ["planned", "building", "none"] ("none" =
|
|
116
|
+
cards with no status set, which usually means unbuilt). traverse takes the same status
|
|
117
|
+
filter as a post-filter: the walk still passes through built cards, so a built hub never
|
|
118
|
+
hides the planned work behind it.
|
|
110
119
|
|
|
111
120
|
The plan folder is found by walking up from the working directory, BOUNDED by the repo
|
|
112
121
|
root (it never adopts another repo's plan). If no plan exists in this repo (tools return
|
|
@@ -281,9 +290,7 @@ async function computeStaleCards(root, index, base) {
|
|
|
281
290
|
marker = null;
|
|
282
291
|
}
|
|
283
292
|
const fallback = base ?? marker ?? null;
|
|
284
|
-
const
|
|
285
|
-
const noBaseline = [];
|
|
286
|
-
let checked = 0;
|
|
293
|
+
const claims = [];
|
|
287
294
|
for (const card of index.cards.values()) {
|
|
288
295
|
const verifiedSha = typeof card.frontmatter.verified_sha === 'string'
|
|
289
296
|
? card.frontmatter.verified_sha
|
|
@@ -293,20 +300,39 @@ async function computeStaleCards(root, index, base) {
|
|
|
293
300
|
continue;
|
|
294
301
|
if (boundPathsForCard(index, card).length === 0)
|
|
295
302
|
continue;
|
|
296
|
-
checked++;
|
|
297
|
-
const baseline = verifiedSha ?? fallback ?? undefined;
|
|
298
303
|
const resolved = await resolveCodeForCard(root, index, card, 'paths');
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
claims.push({
|
|
305
|
+
card,
|
|
306
|
+
baseline: verifiedSha ?? fallback,
|
|
307
|
+
baseline_source: verifiedSha ? 'verified_sha' : base ? 'argument' : 'sync-marker',
|
|
308
|
+
paths: resolved.files.map((f) => f.path),
|
|
309
|
+
missing: resolved.files.filter((f) => !f.exists).map((f) => f.path),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// Pass 2: one git call per DISTINCT baseline (usually just the sync marker),
|
|
313
|
+
// not one per card — a plan with 100 verified cards must not spawn 100 diffs.
|
|
314
|
+
const changedBy = new Map();
|
|
315
|
+
const baselines = new Set(claims.map((c) => c.baseline).filter((b) => Boolean(b)));
|
|
316
|
+
for (const baseline of baselines) {
|
|
317
|
+
const union = [
|
|
318
|
+
...new Set(claims.filter((c) => c.baseline === baseline).flatMap((c) => c.paths)),
|
|
319
|
+
];
|
|
320
|
+
try {
|
|
321
|
+
changedBy.set(baseline, await changedFilesSince(root, baseline, union));
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
changedBy.set(baseline, 'unreachable');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const stale = [];
|
|
328
|
+
const noBaseline = [];
|
|
329
|
+
for (const { card, baseline, baseline_source, paths, missing } of claims) {
|
|
301
330
|
if (!baseline) {
|
|
302
331
|
noBaseline.push({ handle: card.handle, status: card.status ?? null, files: paths });
|
|
303
332
|
continue;
|
|
304
333
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
changed = await changedFilesSince(root, baseline, paths);
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
334
|
+
const changed = changedBy.get(baseline);
|
|
335
|
+
if (changed === 'unreachable') {
|
|
310
336
|
noBaseline.push({
|
|
311
337
|
handle: card.handle,
|
|
312
338
|
status: card.status ?? null,
|
|
@@ -322,13 +348,13 @@ async function computeStaleCards(root, index, base) {
|
|
|
322
348
|
name: card.name ?? null,
|
|
323
349
|
status: card.status ?? null,
|
|
324
350
|
baseline: baseline.slice(0, 12),
|
|
325
|
-
baseline_source
|
|
351
|
+
baseline_source,
|
|
326
352
|
changed_files: changedFiles,
|
|
327
353
|
missing_files: missing,
|
|
328
354
|
});
|
|
329
355
|
}
|
|
330
356
|
}
|
|
331
|
-
return { checked, stale, no_baseline: noBaseline };
|
|
357
|
+
return { checked: claims.length, stale, no_baseline: noBaseline };
|
|
332
358
|
}
|
|
333
359
|
// Rough dependency tiers for assemble's suggested build order: data first, then
|
|
334
360
|
// contracts, then surfaces. Connections are undirected, so this is a heuristic
|
|
@@ -387,6 +413,17 @@ function partitionByFiles(seeds, filesBy) {
|
|
|
387
413
|
const detailSchema = z.enum(['none', 'summary', 'full']);
|
|
388
414
|
const typeSchema = z.enum(TYPE_NAMES);
|
|
389
415
|
const statusSchema = z.enum(['planned', 'building', 'built', 'verified']);
|
|
416
|
+
// Filter variant: "none" selects cards with no status at all — unset usually
|
|
417
|
+
// means nobody has claimed the card is built, so backlog queries want it.
|
|
418
|
+
const statusFilterSchema = z.enum(['planned', 'building', 'built', 'verified', 'none']);
|
|
419
|
+
const statusesSchema = z
|
|
420
|
+
.union([statusFilterSchema, z.array(statusFilterSchema).min(1)])
|
|
421
|
+
.optional();
|
|
422
|
+
function statusSetOf(status) {
|
|
423
|
+
if (status === undefined)
|
|
424
|
+
return null;
|
|
425
|
+
return new Set(Array.isArray(status) ? status : [status]);
|
|
426
|
+
}
|
|
390
427
|
const noteKindSchema = z.enum(['decision', 'gotcha', 'state', 'deviation', 'verified']);
|
|
391
428
|
const codeModeSchema = z.enum(['none', 'paths', 'direct']);
|
|
392
429
|
const repoSchema = z
|
|
@@ -504,7 +541,7 @@ export function buildServer(options = {}) {
|
|
|
504
541
|
});
|
|
505
542
|
server.registerTool('get_card', {
|
|
506
543
|
annotations: { readOnlyHint: true },
|
|
507
|
-
description: 'Fetch one card by handle, optionally with all connected cards hydrated. connected: "full" returns the complete frontmatter and body of every connected card — use it when about to work on an area. code: "paths" returns the resolved file paths the card is bound to (connected FILE cards plus code_refs); code: "direct" attaches their contents (
|
|
544
|
+
description: 'Fetch one card by handle, optionally with all connected cards hydrated. connected: "full" returns the complete frontmatter and body of every connected card — use it when about to work on an area. code: "paths" returns the resolved file paths the card is bound to (connected FILE cards plus code_refs); code: "direct" attaches their contents (over-cap files truncated with truncated:true; binaries/lockfiles/generated skipped) so a background coder starts from intent + current code in one call.',
|
|
508
545
|
inputSchema: {
|
|
509
546
|
handle: z.string(),
|
|
510
547
|
connected: detailSchema.optional().describe('default: summary'),
|
|
@@ -548,11 +585,11 @@ export function buildServer(options = {}) {
|
|
|
548
585
|
}));
|
|
549
586
|
server.registerTool('list_cards', {
|
|
550
587
|
annotations: { readOnlyHint: true },
|
|
551
|
-
description: 'Catalog of cards filtered by type, kind, status, and/or connectedness. connected:false returns orphans (cards with zero connections). Returns summaries (handle, type, kind, name, status).',
|
|
588
|
+
description: 'Catalog of cards filtered by type, kind, status, and/or connectedness. status takes one value or a list; "none" selects cards with no status at all — status: ["planned", "building", "none"] is the backlog view (everything not yet built). connected:false returns orphans (cards with zero connections). Returns summaries (handle, type, kind, name, status).',
|
|
552
589
|
inputSchema: {
|
|
553
590
|
types: z.array(typeSchema).optional(),
|
|
554
591
|
kind: z.string().optional(),
|
|
555
|
-
status:
|
|
592
|
+
status: statusesSchema.describe('one status or a list; "none" = cards with no status set'),
|
|
556
593
|
connected: z
|
|
557
594
|
.boolean()
|
|
558
595
|
.optional()
|
|
@@ -563,11 +600,12 @@ export function buildServer(options = {}) {
|
|
|
563
600
|
}, withPlan(async (root, { types, kind, status, connected, limit }) => {
|
|
564
601
|
const index = await loadPlan(root);
|
|
565
602
|
const typeFilter = types && types.length > 0 ? new Set(types) : null;
|
|
603
|
+
const statusFilter = statusSetOf(status);
|
|
566
604
|
const isConnected = (h) => (index.connectedHandles.get(h)?.size ?? 0) > 0;
|
|
567
605
|
const matched = [...index.cards.values()]
|
|
568
606
|
.filter((c) => !typeFilter || typeFilter.has(c.type))
|
|
569
607
|
.filter((c) => !kind || c.kind === kind)
|
|
570
|
-
.filter((c) => !
|
|
608
|
+
.filter((c) => !statusFilter || statusFilter.has(c.status ?? 'none'))
|
|
571
609
|
.filter((c) => connected === undefined || isConnected(c.handle) === connected)
|
|
572
610
|
.sort((a, b) => a.handle.localeCompare(b.handle));
|
|
573
611
|
return ok({
|
|
@@ -599,15 +637,16 @@ export function buildServer(options = {}) {
|
|
|
599
637
|
}));
|
|
600
638
|
server.registerTool('traverse', {
|
|
601
639
|
annotations: { readOnlyHint: true },
|
|
602
|
-
description: 'Breadth-first walk of the connection graph from one or more starting handles. Seed it with diff_plan output for impact analysis. detail: "full" includes frontmatter and body of every reached card.',
|
|
640
|
+
description: 'Breadth-first walk of the connection graph from one or more starting handles. Seed it with diff_plan output for impact analysis. detail: "full" includes frontmatter and body of every reached card. status filters the RESULT only — the walk still passes through non-matching cards, so a built hub never hides the planned work behind it (status: ["planned", "building", "none"] = open work in this neighborhood). types, by contrast, prunes the walk itself.',
|
|
603
641
|
inputSchema: {
|
|
604
642
|
start: z.union([z.string(), z.array(z.string()).min(1)]),
|
|
605
643
|
depth: z.number().int().min(0).max(5).optional().describe('default: 2'),
|
|
606
644
|
types: z.array(typeSchema).optional(),
|
|
645
|
+
status: statusesSchema.describe('post-filter on returned cards; "none" = no status set. The walk passes through non-matching cards.'),
|
|
607
646
|
detail: z.enum(['summary', 'full']).optional().describe('default: summary'),
|
|
608
647
|
repo: repoSchema,
|
|
609
648
|
},
|
|
610
|
-
}, withPlan(async (root, { start, depth, types, detail }) => {
|
|
649
|
+
}, withPlan(async (root, { start, depth, types, status, detail }) => {
|
|
611
650
|
const index = await loadPlan(root);
|
|
612
651
|
const starts = (Array.isArray(start) ? start : [start]).map((s) => s.toUpperCase());
|
|
613
652
|
const missing = starts.filter((s) => !index.cards.has(s));
|
|
@@ -637,7 +676,12 @@ export function buildServer(options = {}) {
|
|
|
637
676
|
}
|
|
638
677
|
frontier = next;
|
|
639
678
|
}
|
|
679
|
+
// Status is a post-filter: the walk above passed THROUGH every card, so
|
|
680
|
+
// a built hub in the middle never hides open work behind it.
|
|
681
|
+
const statusFilter = statusSetOf(status);
|
|
640
682
|
const cards = [...distance.entries()]
|
|
683
|
+
.filter(([handle]) => !statusFilter ||
|
|
684
|
+
statusFilter.has(index.cards.get(handle).status ?? 'none'))
|
|
641
685
|
.map(([handle, dist]) => {
|
|
642
686
|
const card = index.cards.get(handle);
|
|
643
687
|
return {
|
|
@@ -646,7 +690,8 @@ export function buildServer(options = {}) {
|
|
|
646
690
|
};
|
|
647
691
|
})
|
|
648
692
|
.sort((a, b) => a.distance - b.distance || a.handle.localeCompare(b.handle));
|
|
649
|
-
const
|
|
693
|
+
const surviving = new Set(cards.map((c) => c.handle));
|
|
694
|
+
const connections = index.connections.filter((c) => surviving.has(c.a) && surviving.has(c.b));
|
|
650
695
|
return ok({ cards, connections, not_found: missing });
|
|
651
696
|
}));
|
|
652
697
|
server.registerTool('assemble', {
|
|
@@ -674,9 +719,12 @@ export function buildServer(options = {}) {
|
|
|
674
719
|
}, withPlan(async (root, { handles, base, depth, code }) => {
|
|
675
720
|
const index = await loadPlan(root);
|
|
676
721
|
let seeds;
|
|
722
|
+
let notFound = [];
|
|
677
723
|
let delta = null;
|
|
678
724
|
if (handles && handles.length > 0) {
|
|
679
|
-
|
|
725
|
+
const requested = [...new Set(handles.map((h) => h.toUpperCase()))];
|
|
726
|
+
seeds = requested.filter((h) => index.cards.has(h));
|
|
727
|
+
notFound = requested.filter((h) => !index.cards.has(h));
|
|
680
728
|
}
|
|
681
729
|
else {
|
|
682
730
|
let diff;
|
|
@@ -693,6 +741,7 @@ export function buildServer(options = {}) {
|
|
|
693
741
|
return ok({
|
|
694
742
|
base: delta,
|
|
695
743
|
seeds: [],
|
|
744
|
+
not_found: notFound,
|
|
696
745
|
units: [],
|
|
697
746
|
note: handles
|
|
698
747
|
? 'None of the given handles exist in the plan.'
|
|
@@ -744,6 +793,7 @@ export function buildServer(options = {}) {
|
|
|
744
793
|
return ok({
|
|
745
794
|
base: delta,
|
|
746
795
|
seeds,
|
|
796
|
+
not_found: notFound,
|
|
747
797
|
reached_handles: reached,
|
|
748
798
|
suggested_order: suggestedOrder,
|
|
749
799
|
units,
|
|
@@ -960,12 +1010,17 @@ export function buildServer(options = {}) {
|
|
|
960
1010
|
const touched = new Set();
|
|
961
1011
|
for (const [src, targets] of additions) {
|
|
962
1012
|
const card = index.cards.get(src);
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1013
|
+
// Merge against the file's CURRENT list inside the lock, not the index
|
|
1014
|
+
// snapshot — a concurrent write must not be clobbered.
|
|
1015
|
+
await mutateCardFile(card.filePath, (current) => {
|
|
1016
|
+
const existingList = Array.isArray(current.frontmatter.connections)
|
|
1017
|
+
? current.frontmatter.connections.filter((c) => typeof c === 'string')
|
|
1018
|
+
: [];
|
|
1019
|
+
const merged = [...new Set([...existingList, ...targets])];
|
|
1020
|
+
return {
|
|
1021
|
+
frontmatter: applyCardPatch(current.frontmatter, { connections: merged }),
|
|
1022
|
+
};
|
|
1023
|
+
});
|
|
969
1024
|
touched.add(card.relPath);
|
|
970
1025
|
added += targets.size;
|
|
971
1026
|
}
|
|
@@ -1010,10 +1065,12 @@ export function buildServer(options = {}) {
|
|
|
1010
1065
|
return fail('STALE', `${card.handle} changed on disk`);
|
|
1011
1066
|
}
|
|
1012
1067
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1068
|
+
// Apply the patch to the file's CURRENT frontmatter inside the lock —
|
|
1069
|
+
// patch semantics compose with a concurrent write instead of undoing it.
|
|
1070
|
+
await mutateCardFile(card.filePath, (current) => ({
|
|
1071
|
+
frontmatter: patch ? applyCardPatch(current.frontmatter, patch) : undefined,
|
|
1072
|
+
body,
|
|
1073
|
+
}));
|
|
1017
1074
|
const lint = await lintPlan(root);
|
|
1018
1075
|
const updated = lint.index.cards.get(card.handle);
|
|
1019
1076
|
return ok({
|
|
@@ -1041,8 +1098,11 @@ export function buildServer(options = {}) {
|
|
|
1041
1098
|
const note = { kind, text };
|
|
1042
1099
|
if (sha)
|
|
1043
1100
|
note.sha = sha;
|
|
1044
|
-
|
|
1045
|
-
|
|
1101
|
+
// Append to the CURRENT notes list inside the lock so two concurrent
|
|
1102
|
+
// appends both land — the memory tool must never lose a note.
|
|
1103
|
+
await mutateCardFile(card.filePath, (current) => ({
|
|
1104
|
+
frontmatter: withAppendedNote(current.frontmatter, note),
|
|
1105
|
+
}));
|
|
1046
1106
|
const lint = await lintPlan(root);
|
|
1047
1107
|
const updated = lint.index.cards.get(card.handle);
|
|
1048
1108
|
return ok({
|
|
@@ -1053,6 +1113,56 @@ export function buildServer(options = {}) {
|
|
|
1053
1113
|
issues: issuesForFile(lint.issues, card.relPath),
|
|
1054
1114
|
});
|
|
1055
1115
|
}));
|
|
1116
|
+
server.registerTool('list_notes', {
|
|
1117
|
+
annotations: { readOnlyHint: true },
|
|
1118
|
+
description: 'All typed notes across the plan (the memory recorded via append_note), in handle order and newest-last within each card. Filter by kind — "show me every gotcha" / "every decision" in one call — and/or by handles. The cross-card view of the plan\'s memory; for one card\'s notes use get_card notes_kind/notes_limit.',
|
|
1119
|
+
inputSchema: {
|
|
1120
|
+
kind: noteKindSchema.optional().describe('return only notes of this kind'),
|
|
1121
|
+
handles: z
|
|
1122
|
+
.array(z.string())
|
|
1123
|
+
.optional()
|
|
1124
|
+
.describe('restrict to these cards; omit for the whole plan'),
|
|
1125
|
+
limit: z
|
|
1126
|
+
.number()
|
|
1127
|
+
.int()
|
|
1128
|
+
.min(1)
|
|
1129
|
+
.max(500)
|
|
1130
|
+
.optional()
|
|
1131
|
+
.describe('default 200'),
|
|
1132
|
+
repo: repoSchema,
|
|
1133
|
+
},
|
|
1134
|
+
}, withPlan(async (root, { kind, handles, limit }) => {
|
|
1135
|
+
const index = await loadPlan(root);
|
|
1136
|
+
const wanted = handles && handles.length > 0
|
|
1137
|
+
? new Set(handles.map((h) => h.toUpperCase()))
|
|
1138
|
+
: null;
|
|
1139
|
+
const notes = [];
|
|
1140
|
+
const sorted = [...index.cards.values()].sort((a, b) => a.handle.localeCompare(b.handle));
|
|
1141
|
+
for (const card of sorted) {
|
|
1142
|
+
if (wanted && !wanted.has(card.handle))
|
|
1143
|
+
continue;
|
|
1144
|
+
const list = Array.isArray(card.frontmatter.notes)
|
|
1145
|
+
? card.frontmatter.notes
|
|
1146
|
+
: [];
|
|
1147
|
+
for (const n of list) {
|
|
1148
|
+
if (!n || typeof n !== 'object')
|
|
1149
|
+
continue;
|
|
1150
|
+
const note = n;
|
|
1151
|
+
if (typeof note.text !== 'string')
|
|
1152
|
+
continue;
|
|
1153
|
+
const noteKind = typeof note.kind === 'string' ? note.kind : 'note';
|
|
1154
|
+
if (kind && noteKind !== kind)
|
|
1155
|
+
continue;
|
|
1156
|
+
notes.push({
|
|
1157
|
+
handle: card.handle,
|
|
1158
|
+
kind: noteKind,
|
|
1159
|
+
text: note.text,
|
|
1160
|
+
...(typeof note.sha === 'string' ? { sha: note.sha } : {}),
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return ok({ total: notes.length, notes: notes.slice(0, limit ?? 200) });
|
|
1165
|
+
}));
|
|
1056
1166
|
server.registerTool('edit_section', {
|
|
1057
1167
|
description: 'Replace the content under one markdown heading in a card\'s body, keeping every other section byte-for-byte — a cheap, surgical alternative to rewriting the whole body. Match the heading by its text (case-insensitive, no #). Errors if no such heading exists (use update_card to set the whole body or add a section).',
|
|
1058
1168
|
inputSchema: {
|
|
@@ -1068,17 +1178,25 @@ export function buildServer(options = {}) {
|
|
|
1068
1178
|
const card = index.cards.get(handle.toUpperCase());
|
|
1069
1179
|
if (!card)
|
|
1070
1180
|
return fail('NOT_FOUND', `No card with handle ${handle}`);
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
const
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1181
|
+
// Replace against the CURRENT body inside the lock — concurrent edits to
|
|
1182
|
+
// different sections compose instead of the later one clobbering.
|
|
1183
|
+
let failure = null;
|
|
1184
|
+
await mutateCardFile(card.filePath, (current) => {
|
|
1185
|
+
const body = replaceBodySection(current.body, section, text);
|
|
1186
|
+
if (body === null) {
|
|
1187
|
+
const headings = bodyHeadingTexts(current.body);
|
|
1188
|
+
const target = section.trim().replace(/^#+\s*/, '').toLowerCase();
|
|
1189
|
+
const matchCount = headings.filter((h) => h.toLowerCase() === target).length;
|
|
1190
|
+
failure =
|
|
1191
|
+
matchCount > 1
|
|
1192
|
+
? fail('AMBIGUOUS_SECTION', `${card.handle} has ${matchCount} headings called "${section}"; edit_section can't tell them apart. Use update_card to set the whole body.`)
|
|
1193
|
+
: fail('SECTION_NOT_FOUND', `No heading "${section}" in ${card.handle}. Headings present: ${headings.length ? headings.join(', ') : '(none)'}. Use update_card to set the whole body or add the section.`);
|
|
1194
|
+
return null;
|
|
1078
1195
|
}
|
|
1079
|
-
return
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1196
|
+
return { body };
|
|
1197
|
+
});
|
|
1198
|
+
if (failure)
|
|
1199
|
+
return failure;
|
|
1082
1200
|
const lint = await lintPlan(root);
|
|
1083
1201
|
const updated = lint.index.cards.get(card.handle);
|
|
1084
1202
|
return ok({
|
|
@@ -1105,36 +1223,65 @@ export function buildServer(options = {}) {
|
|
|
1105
1223
|
const card = index.cards.get(handle.toUpperCase());
|
|
1106
1224
|
if (!card)
|
|
1107
1225
|
return fail('NOT_FOUND', `No card with handle ${handle}`);
|
|
1226
|
+
const warnings = [];
|
|
1108
1227
|
let resolvedSha = sha;
|
|
1109
|
-
|
|
1110
|
-
|
|
1228
|
+
if (resolvedSha) {
|
|
1229
|
+
// Normalize to the full commit sha; a typo'd baseline would otherwise
|
|
1230
|
+
// surface only later, as an unreachable baseline in stale_report.
|
|
1231
|
+
try {
|
|
1232
|
+
resolvedSha = await resolveCommit(root, resolvedSha);
|
|
1233
|
+
}
|
|
1234
|
+
catch {
|
|
1235
|
+
warnings.push(`sha ${resolvedSha} does not resolve to a commit in this repo; stamped as given — stale_report will report this card's baseline as unreachable until it does.`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1111
1239
|
try {
|
|
1112
1240
|
resolvedSha = await headSha(root);
|
|
1113
1241
|
}
|
|
1114
1242
|
catch {
|
|
1115
|
-
|
|
1116
|
-
|
|
1243
|
+
warnings.push('Not a git repo (or no commits): stamped verified_at + status only. Drift detection needs a verified_sha baseline — pass sha or commit first.');
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
// A verified_sha is a claim about COMMITTED code. Uncommitted edits to the
|
|
1247
|
+
// bound files are not covered by it — mirror set_sync_point's dirty warning.
|
|
1248
|
+
if (resolvedSha) {
|
|
1249
|
+
try {
|
|
1250
|
+
const bound = boundPathsForCard(index, card).map((b) => b.path);
|
|
1251
|
+
const dirty = await changedFilesSince(root, 'HEAD', bound);
|
|
1252
|
+
const dirtyBound = bound.filter((p) => dirty.has(p));
|
|
1253
|
+
if (dirtyBound.length > 0) {
|
|
1254
|
+
warnings.push(`Bound file(s) have uncommitted changes the baseline does not include: ${dirtyBound.join(', ')}. Commit first, then set_verified.`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
catch {
|
|
1258
|
+
// Best-effort: no git repo or nothing bound — nothing to warn about.
|
|
1117
1259
|
}
|
|
1118
1260
|
}
|
|
1119
1261
|
const verifiedAt = new Date().toISOString();
|
|
1120
1262
|
const fields = { verified_at: verifiedAt };
|
|
1121
1263
|
if (resolvedSha)
|
|
1122
1264
|
fields.verified_sha = resolvedSha;
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1265
|
+
await mutateCardFile(card.filePath, (current) => {
|
|
1266
|
+
let frontmatter = applyCardPatch(current.frontmatter, {
|
|
1267
|
+
status: 'verified',
|
|
1268
|
+
fields,
|
|
1269
|
+
});
|
|
1270
|
+
if (note) {
|
|
1271
|
+
const n = { kind: 'verified', text: note };
|
|
1272
|
+
if (resolvedSha)
|
|
1273
|
+
n.sha = resolvedSha;
|
|
1274
|
+
frontmatter = withAppendedNote(frontmatter, n);
|
|
1275
|
+
}
|
|
1276
|
+
return { frontmatter };
|
|
1277
|
+
});
|
|
1131
1278
|
const lint = await lintPlan(root);
|
|
1132
1279
|
const updated = lint.index.cards.get(card.handle);
|
|
1133
1280
|
return ok({
|
|
1134
1281
|
card: updated ? full(updated) : null,
|
|
1135
1282
|
verified_sha: resolvedSha ?? null,
|
|
1136
1283
|
verified_at: verifiedAt,
|
|
1137
|
-
warning,
|
|
1284
|
+
warning: warnings.length > 0 ? warnings.join(' ') : undefined,
|
|
1138
1285
|
issues: issuesForFile(lint.issues, card.relPath),
|
|
1139
1286
|
});
|
|
1140
1287
|
}));
|
|
@@ -1158,6 +1305,38 @@ export function buildServer(options = {}) {
|
|
|
1158
1305
|
issues: lint.issues.filter((i) => handleToken.test(i.message)),
|
|
1159
1306
|
});
|
|
1160
1307
|
}));
|
|
1308
|
+
server.registerTool('rename_card', {
|
|
1309
|
+
description: 'Rename a card\'s handle and rewrite every reference to it across the plan — connections lists, handle-shaped frontmatter values, [[links]], mermaid nodes, and prose mentions (whole-token matches only; API-USER never touches API-USERS). The file moves to the new handle\'s path, so a cross-type rename (new prefix) also moves folders and the card\'s fields are then validated against the new type\'s schema. Returns the handles whose references were rewritten plus lint issues for every touched file.',
|
|
1310
|
+
inputSchema: { from: z.string(), to: z.string(), repo: repoSchema },
|
|
1311
|
+
}, withPlan(async (root, args) => {
|
|
1312
|
+
let result;
|
|
1313
|
+
try {
|
|
1314
|
+
result = await renameCard(root, args.from, args.to);
|
|
1315
|
+
}
|
|
1316
|
+
catch (err) {
|
|
1317
|
+
if (err instanceof RenameCardError)
|
|
1318
|
+
return fail(err.code, err.message);
|
|
1319
|
+
throw err;
|
|
1320
|
+
}
|
|
1321
|
+
if (result.noop) {
|
|
1322
|
+
return ok({ renamed: null, note: 'from and to are the same handle — nothing to do.' });
|
|
1323
|
+
}
|
|
1324
|
+
const lint = await lintPlan(root);
|
|
1325
|
+
const touched = new Set([result.file]);
|
|
1326
|
+
for (const h of result.references_updated) {
|
|
1327
|
+
const rel = lint.index.cards.get(h)?.relPath;
|
|
1328
|
+
if (rel)
|
|
1329
|
+
touched.add(rel);
|
|
1330
|
+
}
|
|
1331
|
+
const renamed = lint.index.cards.get(result.to);
|
|
1332
|
+
return ok({
|
|
1333
|
+
renamed: { from: result.from, to: result.to },
|
|
1334
|
+
file: result.file,
|
|
1335
|
+
references_updated: result.references_updated,
|
|
1336
|
+
card: renamed ? full(renamed) : null,
|
|
1337
|
+
issues: lint.issues.filter((i) => touched.has(i.file)),
|
|
1338
|
+
});
|
|
1339
|
+
}));
|
|
1161
1340
|
server.registerTool('add_connection', {
|
|
1162
1341
|
description: 'Connect two cards by appending `to` to `from`’s connections list. No-op if they are already connected through any source.',
|
|
1163
1342
|
inputSchema: { from: z.string(), to: z.string(), repo: repoSchema },
|
|
@@ -1171,13 +1350,18 @@ export function buildServer(options = {}) {
|
|
|
1171
1350
|
if (index.connectedHandles.get(from.handle)?.has(to.handle)) {
|
|
1172
1351
|
return ok({ already_connected: true, between: [from.handle, to.handle] });
|
|
1173
1352
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1353
|
+
await mutateCardFile(from.filePath, (current) => {
|
|
1354
|
+
const existing = Array.isArray(current.frontmatter.connections)
|
|
1355
|
+
? current.frontmatter.connections.filter((c) => typeof c === 'string')
|
|
1356
|
+
: [];
|
|
1357
|
+
if (existing.includes(to.handle))
|
|
1358
|
+
return null; // raced: already added
|
|
1359
|
+
return {
|
|
1360
|
+
frontmatter: deepMerge(current.frontmatter, {
|
|
1361
|
+
connections: [...existing, to.handle],
|
|
1362
|
+
}),
|
|
1363
|
+
};
|
|
1179
1364
|
});
|
|
1180
|
-
await updateCardFile(from.filePath, { frontmatter });
|
|
1181
1365
|
const lint = await lintPlan(root);
|
|
1182
1366
|
return ok({
|
|
1183
1367
|
connected: [from.handle, to.handle],
|
|
@@ -1200,17 +1384,24 @@ export function buildServer(options = {}) {
|
|
|
1200
1384
|
[cardA, cardB.handle],
|
|
1201
1385
|
[cardB, cardA.handle],
|
|
1202
1386
|
]) {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1387
|
+
let removed = false;
|
|
1388
|
+
await mutateCardFile(card.filePath, (current) => {
|
|
1389
|
+
// Keep malformed (non-string) entries as-is — lint owns reporting them.
|
|
1390
|
+
const list = Array.isArray(current.frontmatter.connections)
|
|
1391
|
+
? current.frontmatter.connections
|
|
1392
|
+
: [];
|
|
1393
|
+
if (!list.includes(other))
|
|
1394
|
+
return null;
|
|
1395
|
+
removed = true;
|
|
1207
1396
|
const next = list.filter((h) => h !== other);
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1397
|
+
return {
|
|
1398
|
+
frontmatter: deepMerge(current.frontmatter, {
|
|
1399
|
+
connections: next.length > 0 ? next : null,
|
|
1400
|
+
}),
|
|
1401
|
+
};
|
|
1402
|
+
});
|
|
1403
|
+
if (removed)
|
|
1212
1404
|
removedFrom.push(card.handle);
|
|
1213
|
-
}
|
|
1214
1405
|
}
|
|
1215
1406
|
const lint = await lintPlan(root);
|
|
1216
1407
|
const after = lint.index;
|
|
@@ -1285,11 +1476,15 @@ export function buildServer(options = {}) {
|
|
|
1285
1476
|
return fail('NO_PLAN_PROJECT', 'No plan.md (PLAN-PROJECT) at the plan root to record connected_repos on.');
|
|
1286
1477
|
}
|
|
1287
1478
|
const entry = { name, path: repoPath, description };
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1479
|
+
let next = [];
|
|
1480
|
+
await mutateCardFile(planCard.filePath, (current) => {
|
|
1481
|
+
next = upsertConnectedRepo(connectedReposFromFrontmatter(current.frontmatter), entry);
|
|
1482
|
+
return {
|
|
1483
|
+
frontmatter: applyCardPatch(current.frontmatter, {
|
|
1484
|
+
fields: { connected_repos: next.map(connectedRepoToFm) },
|
|
1485
|
+
}),
|
|
1486
|
+
};
|
|
1291
1487
|
});
|
|
1292
|
-
await updateCardFile(planCard.filePath, { frontmatter });
|
|
1293
1488
|
let reciprocated;
|
|
1294
1489
|
if (reciprocate) {
|
|
1295
1490
|
const target = await resolveConnectedRepo(root, repoPath);
|
|
@@ -1313,12 +1508,13 @@ export function buildServer(options = {}) {
|
|
|
1313
1508
|
path: path.relative(target.repoRoot, homeRepoRoot) || '.',
|
|
1314
1509
|
description: reverse_description,
|
|
1315
1510
|
};
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1511
|
+
await mutateCardFile(targetPlan.filePath, (current) => ({
|
|
1512
|
+
frontmatter: applyCardPatch(current.frontmatter, {
|
|
1513
|
+
fields: {
|
|
1514
|
+
connected_repos: upsertConnectedRepo(connectedReposFromFrontmatter(current.frontmatter), reverseEntry).map(connectedRepoToFm),
|
|
1515
|
+
},
|
|
1320
1516
|
}),
|
|
1321
|
-
});
|
|
1517
|
+
}));
|
|
1322
1518
|
reciprocated = {
|
|
1323
1519
|
ok: true,
|
|
1324
1520
|
repo_root: target.repoRoot,
|
|
@@ -1346,11 +1542,16 @@ export function buildServer(options = {}) {
|
|
|
1346
1542
|
if (!existing.some((r) => r.name === name)) {
|
|
1347
1543
|
return ok({ removed: false, connected_repos: existing.map(connectedRepoToFm) });
|
|
1348
1544
|
}
|
|
1349
|
-
|
|
1350
|
-
await
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1545
|
+
let next = [];
|
|
1546
|
+
await mutateCardFile(planCard.filePath, (current) => {
|
|
1547
|
+
next = removeConnectedRepoEntry(connectedReposFromFrontmatter(current.frontmatter), name);
|
|
1548
|
+
return {
|
|
1549
|
+
frontmatter: applyCardPatch(current.frontmatter, {
|
|
1550
|
+
fields: {
|
|
1551
|
+
connected_repos: next.length > 0 ? next.map(connectedRepoToFm) : null,
|
|
1552
|
+
},
|
|
1553
|
+
}),
|
|
1554
|
+
};
|
|
1354
1555
|
});
|
|
1355
1556
|
const lint = await lintPlan(root);
|
|
1356
1557
|
return ok({
|
|
@@ -1448,9 +1649,9 @@ export function buildServer(options = {}) {
|
|
|
1448
1649
|
repo: repoSchema,
|
|
1449
1650
|
},
|
|
1450
1651
|
}, withPlan(async (root, { base }) => {
|
|
1451
|
-
const
|
|
1452
|
-
const
|
|
1453
|
-
const r = await computeStaleCards(root, index, base);
|
|
1652
|
+
const lint = await lintPlan(root);
|
|
1653
|
+
const status = await computeSyncStatus(root, { lint });
|
|
1654
|
+
const r = await computeStaleCards(root, lint.index, base);
|
|
1454
1655
|
return ok({
|
|
1455
1656
|
advisory: 'Advisory only — the MCP server reports sync state, it cannot block. Use as a definition-of-done gate before calling work complete.',
|
|
1456
1657
|
state: status.state,
|