kortext 2.2.3 → 3.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/LICENSE +21 -0
- package/README.md +175 -65
- package/agents/architect.md +85 -0
- package/agents/compliance-expert.md +36 -75
- package/agents/copywriter.md +36 -92
- package/agents/db-admin.md +43 -91
- package/agents/designer.md +40 -77
- package/agents/devops-engineer.md +37 -113
- package/agents/growth-expert.md +45 -88
- package/agents/product-manager.md +38 -83
- package/agents/qa-engineer.md +51 -99
- package/agents/security-engineer.md +47 -92
- package/bin/kortext.js +2 -0
- package/dist/app.js +829 -0
- package/dist/cli-spawn.js +137 -0
- package/dist/daemon.js +40 -0
- package/dist/db.js +78 -0
- package/dist/design-preview.js +533 -0
- package/dist/docs.js +701 -0
- package/dist/engines.js +225 -0
- package/dist/index.js +109 -0
- package/dist/pick-directory.js +38 -0
- package/dist/projects.js +189 -0
- package/dist/readiness.js +288 -0
- package/dist/runner.js +771 -0
- package/dist/update.js +55 -0
- package/docs/CHANGELOG.md +11 -0
- package/package.json +53 -13
- package/templates/AGENTS.md +62 -0
- package/templates/docs/API.md +67 -0
- package/templates/docs/ARCHITECTURE.md +31 -0
- package/templates/docs/BRIEF.md +27 -0
- package/templates/docs/CONTENT.md +66 -0
- package/templates/docs/DATABASE.md +63 -0
- package/templates/docs/DESIGN.md +124 -0
- package/templates/docs/ENGINEERING.md +66 -0
- package/templates/docs/ENVIRONMENT.md +102 -0
- package/templates/docs/GROWTH.md +44 -0
- package/templates/docs/LEGAL.md +48 -0
- package/templates/docs/PRODUCT.md +43 -0
- package/templates/docs/SECURITY.md +65 -0
- package/templates/docs/STACK.md +57 -0
- package/templates/docs/STRUCTURE.md +73 -0
- package/templates/docs/TEST.md +62 -0
- package/ui/dist/assets/abnfDiagram-VCTEODGH-Bm7Q2BML.js +1 -0
- package/ui/dist/assets/arc-BV9hs0Hu.js +1 -0
- package/ui/dist/assets/architecture-7GRP2DOG-RckR3gBI.js +1 -0
- package/ui/dist/assets/architectureDiagram-5GKGNRK7-CHiJ6JFT.js +36 -0
- package/ui/dist/assets/array-BifhSqXX.js +1 -0
- package/ui/dist/assets/blockDiagram-I7D4REHJ-qCbaKVhd.js +129 -0
- package/ui/dist/assets/c4Diagram-7LVT6UL2-DidCDJrv.js +38 -0
- package/ui/dist/assets/channel-CNUQ11ne.js +1 -0
- package/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/ui/dist/assets/chunk-4HAMMTFA-CDGFjfVp.js +62 -0
- package/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/ui/dist/assets/chunk-75Z2AOVW-CbNT4sDh.js +2 -0
- package/ui/dist/assets/chunk-DU6HZSFF-Cq2BVv26.js +127 -0
- package/ui/dist/assets/chunk-F27PBJKO-CE365m0v.js +1 -0
- package/ui/dist/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
- package/ui/dist/assets/chunk-GMAD6QVW-DxWL1VC-.js +72 -0
- package/ui/dist/assets/chunk-GVQU2GXP-aARV8YbK.js +1 -0
- package/ui/dist/assets/chunk-IMKFNOWR-CC790Kt2.js +231 -0
- package/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/ui/dist/assets/chunk-L3NEJ4N5-BfE4VE8J.js +1 -0
- package/ui/dist/assets/chunk-OSK3NFVY-BImQbSWH.js +10 -0
- package/ui/dist/assets/chunk-P2QGCYS3-CN3XD313.js +1 -0
- package/ui/dist/assets/chunk-POPQ4Y6H-Dqb6jOrX.js +1 -0
- package/ui/dist/assets/chunk-PWAF6VOD-DfXX6rBk.js +1 -0
- package/ui/dist/assets/chunk-SHT3W25Y-DATyyqPK.js +168 -0
- package/ui/dist/assets/chunk-SVP7TREG-r1chwRmg.js +88 -0
- package/ui/dist/assets/chunk-TICWLB2K-DL4atHUC.js +206 -0
- package/ui/dist/assets/chunk-XXDRQBXY-DXrM6jh7.js +1 -0
- package/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/ui/dist/assets/classDiagram-ZZMXUADV-BpQxfIQ7.js +1 -0
- package/ui/dist/assets/classDiagram-v2-VYDZK3BY-BpQxfIQ7.js +1 -0
- package/ui/dist/assets/cose-bilkent-JH36ORCC-Bhe-ZBlS.js +1 -0
- package/ui/dist/assets/cynefin-OW5HDTMX-D1koLd-G.js +1 -0
- package/ui/dist/assets/cynefinDiagram-5FMLGOSQ-BMcw40hV.js +62 -0
- package/ui/dist/assets/cytoscape.esm-CGd-uY3x.js +321 -0
- package/ui/dist/assets/dagre-GXQ25YYZ-DBun-Rs2.js +4 -0
- package/ui/dist/assets/dagre-PrKaheQc.js +1 -0
- package/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/ui/dist/assets/diagram-S7CK7UJ4-C1fUIGvo.js +30 -0
- package/ui/dist/assets/diagram-UQ7AKVKN-B5FDfkvB.js +41 -0
- package/ui/dist/assets/diagram-VSXAHHWV-CCIpOCNC.js +3 -0
- package/ui/dist/assets/diagram-VX7I27RA-DV0q2xo_.js +24 -0
- package/ui/dist/assets/diagram-Z3DM3KII-B3LCdNgI.js +24 -0
- package/ui/dist/assets/dist-EcW76Zvr.js +1 -0
- package/ui/dist/assets/ebnfDiagram-PWID7BFC-De_oRbYe.js +1 -0
- package/ui/dist/assets/erDiagram-RLTQ6QDP-B9NikMr1.js +99 -0
- package/ui/dist/assets/eventmodeling-NTZA5JFV-DZ4t581k.js +1 -0
- package/ui/dist/assets/flowDiagram-HODETNUW-DuTOJp1C.js +1 -0
- package/ui/dist/assets/ganttDiagram-EL5Y4UJY-BVTqqlPw.js +292 -0
- package/ui/dist/assets/gitGraph-4MIJSDKK-xRFaEJdZ.js +1 -0
- package/ui/dist/assets/gitGraphDiagram-WWUBYQGX-CK5_GCmX.js +106 -0
- package/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/ui/dist/assets/index-CjkpNftW.js +46 -0
- package/ui/dist/assets/index-DVIDTWin.css +1 -0
- package/ui/dist/assets/info-A6RAGUB7-Dg7z-CTI.js +1 -0
- package/ui/dist/assets/infoDiagram-27XIBGKW-YaqsOXh0.js +2 -0
- package/ui/dist/assets/init-C-OQMol4.js +1 -0
- package/ui/dist/assets/ishikawaDiagram-5VMMS53U-C-wtxZPa.js +70 -0
- package/ui/dist/assets/journeyDiagram-3NMN7TZE-DsXSXf16.js +139 -0
- package/ui/dist/assets/kanban-definition-UXKFOSKX-Ct59fuWk.js +89 -0
- package/ui/dist/assets/katex-CXMH3UgJ.js +257 -0
- package/ui/dist/assets/line-BUVw9Mhs.js +1 -0
- package/ui/dist/assets/linear-D8ncLO0X.js +1 -0
- package/ui/dist/assets/mermaid-parser.core-BDC4bG-O.js +7 -0
- package/ui/dist/assets/mermaid.core-DZY3WwzX.js +44 -0
- package/ui/dist/assets/mindmap-definition-YA3MSWOX-DtiuF6Ce.js +96 -0
- package/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/ui/dist/assets/packet-AYTQ26CC-BX2-w9rD.js +1 -0
- package/ui/dist/assets/path-fybaL0A-.js +1 -0
- package/ui/dist/assets/pegDiagram-XKGWAZYB-CotwuIHB.js +1 -0
- package/ui/dist/assets/pie-WAS4IAKB-Hth8ZVpm.js +1 -0
- package/ui/dist/assets/pieDiagram-E7YTZNPT-D9-INs8B.js +39 -0
- package/ui/dist/assets/quadrantDiagram-AXDQQJYC-CMtBXZGV.js +7 -0
- package/ui/dist/assets/radar-RG4KPBEZ-6UuwQPsw.js +1 -0
- package/ui/dist/assets/railroad-74A4TZTK-BF5TZgj0.js +1 -0
- package/ui/dist/assets/railroad-abnf-HS5TGJTU-3KYovtYT.js +1 -0
- package/ui/dist/assets/railroad-ebnf-LZEXJU2U-DeP4WedE.js +1 -0
- package/ui/dist/assets/railroad-peg-WCYAUIDC-i42N1-yk.js +1 -0
- package/ui/dist/assets/railroadDiagram-O6MQD6OU-BS6zkU3Z.js +1 -0
- package/ui/dist/assets/requirementDiagram-BXWQKSXE-J4IOB-kP.js +84 -0
- package/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/ui/dist/assets/sankeyDiagram-P5KCCOFB-jYTCuYzQ.js +40 -0
- package/ui/dist/assets/sequenceDiagram-WJ2MYXX4-Cd2r6PW-.js +162 -0
- package/ui/dist/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/ui/dist/assets/src-7TsfZXRO.js +1 -0
- package/ui/dist/assets/stateDiagram-D77RDMKH-DXOdtOKe.js +1 -0
- package/ui/dist/assets/stateDiagram-v2-MP3YSRHH-9h7sIf3R.js +1 -0
- package/ui/dist/assets/swimlanes-42K2YHIH-BvJGmGs1.js +1 -0
- package/ui/dist/assets/swimlanesDiagram-VR7AAH4N-QA4gPi6a.js +8 -0
- package/ui/dist/assets/timeline-definition-24CTP7MA-CAaD6-yd.js +120 -0
- package/ui/dist/assets/treeView-Q6P3EWNA-4gDEXIyJ.js +1 -0
- package/ui/dist/assets/treemap-WGGIJYW6-CGel5TwW.js +1 -0
- package/ui/dist/assets/vennDiagram-4TSXK5OY-DpxsKboL.js +34 -0
- package/ui/dist/assets/wardley-WFR3VGLG-Ck38v5ns.js +1 -0
- package/ui/dist/assets/wardleyDiagram-VM6X3IG4-Ba0grrqs.js +78 -0
- package/ui/dist/assets/xychartDiagram-S5SC5T6Z-DH_gC96K.js +7 -0
- package/ui/dist/index.html +13 -0
- package/ui/dist/kortext-logo-dark.svg +41 -0
- package/ui/dist/kortext-logo.svg +41 -0
- package/workflows/existing-project-analysis.md +87 -0
- package/workflows/new-project-analysis.md +98 -0
- package/workflows/planning-pipeline.md +61 -0
- package/AGENTS.md +0 -28
- package/agents/backend-developer.md +0 -132
- package/agents/delivery-manager.md +0 -121
- package/agents/engineering-manager.md +0 -208
- package/agents/frontend-developer.md +0 -174
- package/agents/operation-manager.md +0 -122
- package/hooks/audit-logger.sh +0 -25
- package/hooks/auto-locker.sh +0 -74
- package/hooks/auto-unlocker.sh +0 -17
- package/hooks/backlog-sync-guard.sh +0 -12
- package/hooks/branch-guard.sh +0 -32
- package/hooks/commit-msg-guard.sh +0 -43
- package/hooks/git-pre-commit.sh +0 -76
- package/hooks/git-pre-push.sh +0 -17
- package/hooks/handover-guard.sh +0 -33
- package/hooks/kortext-init.sh +0 -194
- package/hooks/kortext-lib.sh +0 -151
- package/hooks/lint-guard.sh +0 -50
- package/hooks/secret-scanner.sh +0 -89
- package/hooks/size-guard.sh +0 -48
- package/hooks/snapshot-guard.sh +0 -39
- package/hooks/write-guard.sh +0 -77
- package/rules/behavior.md +0 -252
- package/rules/branching.md +0 -82
- package/rules/commands.md +0 -112
- package/rules/emergency.md +0 -160
- package/rules/mcp.md +0 -47
- package/rules/models.md +0 -93
- package/scripts/kortext-backlog-add.py +0 -123
- package/scripts/kortext-backlog-health.py +0 -112
- package/scripts/kortext-backlog-sync.py +0 -117
- package/scripts/kortext-bulk-plan.py +0 -63
- package/scripts/kortext-cli.py +0 -376
- package/scripts/kortext-consistency-check.py +0 -74
- package/scripts/kortext-context-check.py +0 -146
- package/scripts/kortext-handover.py +0 -107
- package/scripts/kortext-item-check.py +0 -61
- package/scripts/kortext-item-start.py +0 -70
- package/scripts/kortext-item-transition.py +0 -78
- package/scripts/kortext-lock.py +0 -174
- package/scripts/kortext-session-start.py +0 -127
- package/scripts/lock_kortext.sh +0 -34
- package/settings/.claude-settings.template.json +0 -48
- package/settings/CHANGELOG.md +0 -176
- package/settings/INTEGRATION-MAP.md +0 -276
- package/settings/README.md +0 -255
- package/settings/USER-GUIDE.md +0 -502
- package/settings/VERSION +0 -1
- package/settings/config.md +0 -8
- package/settings/runtime-adapters.md +0 -104
- package/skills/backend-developer/.gitkeep +0 -0
- package/skills/compliance-expert/.gitkeep +0 -0
- package/skills/copywriter/.gitkeep +0 -0
- package/skills/db-admin/.gitkeep +0 -0
- package/skills/delivery-manager/.gitkeep +0 -0
- package/skills/designer/.gitkeep +0 -0
- package/skills/devops-engineer/.gitkeep +0 -0
- package/skills/engineering-manager/.gitkeep +0 -0
- package/skills/frontend-developer/.gitkeep +0 -0
- package/skills/growth-expert/.gitkeep +0 -0
- package/skills/operation-manager/.gitkeep +0 -0
- package/skills/product-manager/.gitkeep +0 -0
- package/skills/qa-engineer/.gitkeep +0 -0
- package/skills/security-engineer/.gitkeep +0 -0
- package/workflows/00-kortext-setup.md +0 -53
- package/workflows/01a-analysis-pipeline.md +0 -117
- package/workflows/01b-onboarding-pipeline.md +0 -110
- package/workflows/02-planning-pipeline.md +0 -146
- package/workflows/02b-spike-workflow.md +0 -120
- package/workflows/03-environment-setup.md +0 -99
- package/workflows/04-development-cycle.md +0 -203
- package/workflows/05-test-cycle.md +0 -123
- package/workflows/06-deployment-cycle.md +0 -224
- package/workflows/07-rollback-pipeline.md +0 -203
- package/workflows/08-hotfix-pipeline.md +0 -196
- package/workflows/09-maintenance-cycle.md +0 -133
- package/workspace/archive/.gitkeep +0 -0
- package/workspace/backups/.gitkeep +0 -0
- package/workspace/memory/backlog/.gitkeep +0 -0
- package/workspace/memory/backlog/BXX-[bug-name].md +0 -2
- package/workspace/memory/backlog/DXX-[debt-name].md +0 -2
- package/workspace/memory/backlog/README.md +0 -31
- package/workspace/memory/backlog/TXX-[task-name].md +0 -2
- package/workspace/memory/backlog/debt-dashboard.md +0 -3
- package/workspace/memory/backlog/epic-dashboard.md +0 -10
- package/workspace/memory/backlog/version-dashboard.md +0 -7
- package/workspace/memory/context/.gitkeep +0 -0
- package/workspace/memory/context/README.md +0 -35
- package/workspace/memory/context/[agent-name]-active.md +0 -2
- package/workspace/memory/decisions.md +0 -17
- package/workspace/memory/handover.md +0 -40
- package/workspace/memory/learned.md +0 -21
- package/workspace/references/access.md +0 -47
- package/workspace/references/api-reference.md +0 -59
- package/workspace/references/blueprint.md +0 -29
- package/workspace/references/content-strategy.md +0 -39
- package/workspace/references/db-schema.md +0 -51
- package/workspace/references/design-system.md +0 -107
- package/workspace/references/dictionary.md +0 -42
- package/workspace/references/file-system.md +0 -42
- package/workspace/references/growth-strategy.md +0 -46
- package/workspace/references/legal-strategy.md +0 -35
- package/workspace/references/required-skills.md +0 -11
- package/workspace/references/security-rules.md +0 -34
- package/workspace/references/tech-stack.md +0 -49
- package/workspace/references/test-strategy.md +0 -48
- package/workspace/reports/analysis-reports.md +0 -40
- package/workspace/reports/content-reports.md +0 -29
- package/workspace/reports/delivery-reports.md +0 -26
- package/workspace/reports/growth-reports.md +0 -9
- package/workspace/reports/legal-reports.md +0 -29
- package/workspace/reports/product-requirements.md +0 -28
- package/workspace/reports/release-notes.md +0 -20
- package/workspace/reports/security-reports.md +0 -27
- package/workspace/reports/status-reports.md +0 -48
- package/workspace/reports/tech-requirements.md +0 -30
- package/workspace/reports/test-reports.md +0 -61
- package/workspace/templates/BXX-[bug-name].md +0 -34
- package/workspace/templates/DXX-[debt-name].md +0 -35
- package/workspace/templates/EXX-[epic-name].md +0 -48
- package/workspace/templates/HXX-[hotfix-name].md +0 -73
- package/workspace/templates/SXX-[spike-name].md +0 -51
- package/workspace/templates/TXX-[task-name].md +0 -34
- package/workspace/templates/[agent-name]-active.md +0 -4
- package/workspace/templates/[original-name]_[YYYY-MM-DD_HHMMSS].md +0 -5
- package/workspace/templates/adr-XXX.md +0 -66
- package/workspace/templates/debt-dashboard.md +0 -10
- package/workspace/templates/epic-dashboard.md +0 -10
- package/workspace/templates/version-dashboard.md +0 -10
package/dist/docs.js
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Where a document sits and what it is waiting for. Five inputs decide it, and
|
|
6
|
+
* two of them — the queued rechecks and whether a run carries revision notes —
|
|
7
|
+
* live only in the database, so the answer is computed here rather than shipped
|
|
8
|
+
* raw for the panel to reassemble.
|
|
9
|
+
*
|
|
10
|
+
* Order matters: the first match wins.
|
|
11
|
+
*/
|
|
12
|
+
function fileDoc(doc, job) {
|
|
13
|
+
const at = (section, state, detail) => ({
|
|
14
|
+
section,
|
|
15
|
+
state,
|
|
16
|
+
detail,
|
|
17
|
+
});
|
|
18
|
+
// What a run is doing: reading again, the first draft, or a revision of what
|
|
19
|
+
// stands. A recheck that fell over says `failed · recheck`, not `· draft`.
|
|
20
|
+
const pass = job?.kind === 'recheck' ? 'recheck' : job?.isUpdate ? 'revision' : 'draft';
|
|
21
|
+
if (job?.kind === 'doc' && job.status === 'running')
|
|
22
|
+
return at('doing', 'writing', pass);
|
|
23
|
+
// `paused` means one thing: prime stopped it. A run that errored is `failed`,
|
|
24
|
+
// and the two share no button — Continue against Retry.
|
|
25
|
+
if (job?.status === 'stopped')
|
|
26
|
+
return at('doing', 'paused', pass);
|
|
27
|
+
if (job?.status === 'failed')
|
|
28
|
+
return at('needs', 'failed', pass);
|
|
29
|
+
if (doc.status === 'uninitialized')
|
|
30
|
+
return at('todo', 'waiting', 'queue');
|
|
31
|
+
// Every open Action Needed item blocks approval, so one case covers them all:
|
|
32
|
+
// questions left for prime, requests arriving from other documents, and
|
|
33
|
+
// requests this one wants to send. Findings are not among them — they are
|
|
34
|
+
// records written into the document, not decisions owed.
|
|
35
|
+
if (doc.revisionRequests.length > 0 ||
|
|
36
|
+
(doc.status === 'draft' && (doc.openQuestions || doc.outgoing.length > 0)))
|
|
37
|
+
return at('needs', 'waiting', 'review');
|
|
38
|
+
// A recheck in flight is the agent's work too, so it sits in Doing; one that
|
|
39
|
+
// is only queued is a wait. A draft with a recheck queued still owes prime an
|
|
40
|
+
// approval first — the recheck reads whatever prime approves.
|
|
41
|
+
if (job?.kind === 'recheck' && job.status === 'running')
|
|
42
|
+
return at('doing', 'reading', 'recheck');
|
|
43
|
+
if (doc.status === 'draft')
|
|
44
|
+
return at('needs', 'waiting', 'approve');
|
|
45
|
+
if (doc.pendingRecheck)
|
|
46
|
+
return at('todo', 'waiting', 'recheck');
|
|
47
|
+
if (doc.status === 'approved')
|
|
48
|
+
return at('done', 'approved', null);
|
|
49
|
+
return at('done', 'n/a', null);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One grammar for every marked line. Under `## Change Requests` the line has a
|
|
53
|
+
* direction: `` - `TARGET.md` — reason `` is what THIS document asks of another
|
|
54
|
+
* (it leaves when this document is approved), and `` - [ ] from `SOURCE.md` —
|
|
55
|
+
* reason `` is what another document asked of this one (it stays, and is
|
|
56
|
+
* decided here). Findings use the same shape with any path as the subject,
|
|
57
|
+
* because insisting on `.md` is what made a real `.gitignore` finding vanish
|
|
58
|
+
* into prose nobody could act on.
|
|
59
|
+
*/
|
|
60
|
+
const MARKED_LINE = /^\s*[-*+]\s+(?:\[([ xX]?)\]\s*)?(from\s+)?`([^`]+)`\s*[—:-]?\s*(.*)$/;
|
|
61
|
+
/** An outcome trailer: indented and a list item of its own. */
|
|
62
|
+
const TRAILER = /^\s+[-*+] /;
|
|
63
|
+
/**
|
|
64
|
+
* A demand is one item, but an agent may wrap it over several lines. The
|
|
65
|
+
* indented lines that follow are the rest of the sentence unless they are
|
|
66
|
+
* list items themselves, which is what an outcome trailer looks like.
|
|
67
|
+
* Returns the whole reason and the index the item ends at.
|
|
68
|
+
*/
|
|
69
|
+
function foldWrapped(lines, i, first) {
|
|
70
|
+
let reason = first.trim();
|
|
71
|
+
let j = i + 1;
|
|
72
|
+
for (; j < lines.length; j++) {
|
|
73
|
+
const line = lines[j] ?? '';
|
|
74
|
+
if (!/^\s+\S/.test(line) || TRAILER.test(line))
|
|
75
|
+
break;
|
|
76
|
+
reason = `${reason} ${line.trim()}`.trim();
|
|
77
|
+
}
|
|
78
|
+
return [reason, j];
|
|
79
|
+
}
|
|
80
|
+
/** Every marked line under `heading`, settled or not, with its direction. */
|
|
81
|
+
export function parseMarkedList(content, heading) {
|
|
82
|
+
const out = [];
|
|
83
|
+
const lines = content.split('\n');
|
|
84
|
+
let inSection = false;
|
|
85
|
+
for (let i = 0; i < lines.length; i++) {
|
|
86
|
+
const line = lines[i] ?? '';
|
|
87
|
+
const h = line.match(/^#{1,6}\s+(.*)$/);
|
|
88
|
+
if (h) {
|
|
89
|
+
inSection = heading.test((h[1] ?? '').trim());
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!inSection)
|
|
93
|
+
continue;
|
|
94
|
+
const m = line.match(MARKED_LINE);
|
|
95
|
+
if (!m)
|
|
96
|
+
continue;
|
|
97
|
+
const [reason, next] = foldWrapped(lines, i, m[4] ?? '');
|
|
98
|
+
const trailer = lines[next] ?? '';
|
|
99
|
+
i = next - 1;
|
|
100
|
+
out.push({
|
|
101
|
+
subject: m[3].replace(/^\.kortext\//, ''),
|
|
102
|
+
reason,
|
|
103
|
+
incoming: !!m[2],
|
|
104
|
+
settled: (m[1] ?? '').trim().toLowerCase() === 'x',
|
|
105
|
+
outcome: TRAILER.test(trailer) ? trailer.replace(TRAILER, '').trim() : '',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
const DOC_SUBJECT = /^[A-Za-z][\w./-]*\.md$/;
|
|
111
|
+
/**
|
|
112
|
+
* The three headings, each accepting the name it used to carry. Documents
|
|
113
|
+
* written before the rename are still on disk, and a parser that stopped
|
|
114
|
+
* reading them would drop demands nobody would ever see again.
|
|
115
|
+
*/
|
|
116
|
+
export const CHANGE_REQUESTS = /^(change|revision) requests$/i;
|
|
117
|
+
/** The ledger: what prime refused, with the reason. Read by the next writer. */
|
|
118
|
+
export const DECISIONS = /^decisions$/i;
|
|
119
|
+
export const FINDINGS = /^(findings|warnings)$/i;
|
|
120
|
+
export const QUESTIONS = /^(open )?questions( for prime)?$/i;
|
|
121
|
+
/** What other documents asked of this one and prime has not decided yet. */
|
|
122
|
+
export function parseIncoming(content) {
|
|
123
|
+
return parseMarkedList(content, CHANGE_REQUESTS)
|
|
124
|
+
.filter((r) => r.incoming && !r.settled)
|
|
125
|
+
.map((r) => ({ from: r.subject, reason: r.reason }));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* What prime refused here — the `## Decisions` ledger, plus the two shapes
|
|
129
|
+
* older documents carry: a ticked `denied` line still under Change Requests,
|
|
130
|
+
* and the `## Conflicts` section before that.
|
|
131
|
+
*/
|
|
132
|
+
export function parseDenied(content) {
|
|
133
|
+
const ledger = parseMarkedList(content, DECISIONS);
|
|
134
|
+
const inPlace = parseMarkedList(content, CHANGE_REQUESTS).filter((r) => r.incoming && r.settled && /^(denied|dismissed)/i.test(r.outcome));
|
|
135
|
+
return [...ledger, ...inPlace].map((r) => ({ from: r.subject, reason: r.reason }));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* What this document asks of others — the lines the agent wrote while drafting.
|
|
139
|
+
* They travel to the document they name when this one is approved.
|
|
140
|
+
*/
|
|
141
|
+
export function parseOutgoing(content) {
|
|
142
|
+
return parseMarkedList(content, CHANGE_REQUESTS)
|
|
143
|
+
.filter((r) => !r.incoming && !r.settled && DOC_SUBJECT.test(r.subject))
|
|
144
|
+
.map((r) => ({ target: r.subject, reason: r.reason }));
|
|
145
|
+
}
|
|
146
|
+
/** The `## Conflicts` section older documents carry. Counted, never written. */
|
|
147
|
+
export function parseConflicts(content) {
|
|
148
|
+
return parseMarkedList(content, /^conflicts$/i)
|
|
149
|
+
.filter((r) => !r.settled)
|
|
150
|
+
.map((r) => ({ from: r.subject, reason: r.reason }));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Findings about something the document set does not own.
|
|
154
|
+
*
|
|
155
|
+
* A demand aimed at a file that is not a document counts as one wherever it was
|
|
156
|
+
* written. An agent found `.env` tracked in git and filed it under Change
|
|
157
|
+
* Requests against `.gitignore`; the demand parser wants a document, so the line
|
|
158
|
+
* became prose nobody could act on. It is a finding, and it is read as one.
|
|
159
|
+
*/
|
|
160
|
+
export function parseWarnings(content) {
|
|
161
|
+
const own = parseMarkedList(content, FINDINGS).filter((r) => !r.settled);
|
|
162
|
+
const misfiled = parseMarkedList(content, CHANGE_REQUESTS).filter((r) => !r.incoming && !r.settled && !DOC_SUBJECT.test(r.subject));
|
|
163
|
+
return [...own, ...misfiled].map((r) => ({ subject: r.subject, reason: r.reason }));
|
|
164
|
+
}
|
|
165
|
+
// Read questions only from the questions section; ignore template placeholders.
|
|
166
|
+
export function hasOpenQuestions(content) {
|
|
167
|
+
const lines = content.split('\n');
|
|
168
|
+
let inSection = false;
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const heading = line.match(/^#{1,6}\s+(.*)$/);
|
|
171
|
+
if (heading) {
|
|
172
|
+
inSection = QUESTIONS.test((heading[1] ?? '').trim());
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!inSection)
|
|
176
|
+
continue;
|
|
177
|
+
const t = line.trim();
|
|
178
|
+
if (t === '' || /^[-*+]?\s*\[[^\]]*\]$/.test(t))
|
|
179
|
+
continue;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Lines the agent was meant to replace and did not. A real test shipped an
|
|
186
|
+
* approved `DATABASE.md` still carrying `### Table: \`[table_name]\``, because the
|
|
187
|
+
* prompt told it to keep headings verbatim and the heading itself was a pattern.
|
|
188
|
+
*
|
|
189
|
+
* Bracketed prose is normal in a template (`[e.g., PostgreSQL]`), so a line only
|
|
190
|
+
* counts as unfilled when it survives verbatim from the shipped skeleton — plus
|
|
191
|
+
* any heading carrying a bracketed span, which is a pattern wherever it came from.
|
|
192
|
+
*/
|
|
193
|
+
export function unfilledPlaceholders(content, template) {
|
|
194
|
+
const shipped = new Set((template ?? '')
|
|
195
|
+
.split('\n')
|
|
196
|
+
.map((l) => l.trim())
|
|
197
|
+
.filter(Boolean));
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const line of content.split('\n')) {
|
|
200
|
+
const t = line.trim();
|
|
201
|
+
// An alert marker (`[!WARNING]`) and a task box (`[ ]`, `[x]`) are markdown,
|
|
202
|
+
// not blanks to fill — a template ships them and a finished document keeps
|
|
203
|
+
// them. Only the bracketed prose that is left counts.
|
|
204
|
+
const blanks = t.replace(/\[![A-Z]+\]/g, '').replace(/\[[ xX]\]/g, '');
|
|
205
|
+
if (!t || !/\[[^\]]+\]/.test(blanks))
|
|
206
|
+
continue;
|
|
207
|
+
if (/^#{1,6}\s/.test(t) || shipped.has(t))
|
|
208
|
+
out.push(t);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
export function templateFor(pkgRoot, rel) {
|
|
213
|
+
const p = join(pkgRoot, 'templates', 'docs', rel);
|
|
214
|
+
return existsSync(p) ? readFileSync(p, 'utf8') : null;
|
|
215
|
+
}
|
|
216
|
+
// Parses workflow step metadata: numbered steps carrying
|
|
217
|
+
// 1. **+persona:** …
|
|
218
|
+
// - inputs: `.kortext/BRIEF.md`, …
|
|
219
|
+
// - outputs: `.kortext/STACK.md`
|
|
220
|
+
// - approver: +prime
|
|
221
|
+
// Returns one DocStep per output file.
|
|
222
|
+
export function parseWorkflowSteps(md) {
|
|
223
|
+
const steps = [];
|
|
224
|
+
let author = null;
|
|
225
|
+
let inputs = [];
|
|
226
|
+
let outputs = [];
|
|
227
|
+
let approver = null;
|
|
228
|
+
const flush = () => {
|
|
229
|
+
for (const output of outputs)
|
|
230
|
+
steps.push({ output, inputs, author, approver });
|
|
231
|
+
author = null;
|
|
232
|
+
inputs = [];
|
|
233
|
+
outputs = [];
|
|
234
|
+
approver = null;
|
|
235
|
+
};
|
|
236
|
+
for (const line of md.split('\n')) {
|
|
237
|
+
const stepStart = line.match(/^\d+\.\s+\*\*(\+[a-z-]+):?\*\*/);
|
|
238
|
+
if (stepStart) {
|
|
239
|
+
flush();
|
|
240
|
+
author = stepStart[1];
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const paths = (s) => [...s.matchAll(/`\.kortext\/([^`]+)`/g)].map((m) => m[1]);
|
|
244
|
+
if (/^\s*-\s*inputs:/.test(line))
|
|
245
|
+
inputs = paths(line);
|
|
246
|
+
else if (/^\s*-\s*outputs:/.test(line))
|
|
247
|
+
outputs = paths(line);
|
|
248
|
+
else if (/^\s*-\s*approver:/.test(line))
|
|
249
|
+
approver = line.split('approver:')[1].trim();
|
|
250
|
+
}
|
|
251
|
+
flush();
|
|
252
|
+
return steps;
|
|
253
|
+
}
|
|
254
|
+
// The dependency map follows the project's kind: a 'new' project reads
|
|
255
|
+
// new-project-analysis, an 'existing' one existing-project-analysis
|
|
256
|
+
// (planning-pipeline.md declares no document steps of its own).
|
|
257
|
+
export function workflowNameFor(kind) {
|
|
258
|
+
return kind === 'existing' ? 'existing-project-analysis' : 'new-project-analysis';
|
|
259
|
+
}
|
|
260
|
+
export function loadDocMap(pkgRoot, kind = 'new') {
|
|
261
|
+
const map = new Map();
|
|
262
|
+
// Only analysis workflows declare document steps; planning produces separate .kopeng/ files.
|
|
263
|
+
const p = join(pkgRoot, 'workflows', `${workflowNameFor(kind)}.md`);
|
|
264
|
+
if (!existsSync(p))
|
|
265
|
+
return map;
|
|
266
|
+
for (const step of parseWorkflowSteps(readFileSync(p, 'utf8'))) {
|
|
267
|
+
if (!map.has(step.output))
|
|
268
|
+
map.set(step.output, step);
|
|
269
|
+
}
|
|
270
|
+
return map;
|
|
271
|
+
}
|
|
272
|
+
export function readFrontmatter(content) {
|
|
273
|
+
const out = {};
|
|
274
|
+
if (!content.startsWith('---'))
|
|
275
|
+
return out;
|
|
276
|
+
const end = content.indexOf('\n---', 3);
|
|
277
|
+
if (end === -1)
|
|
278
|
+
return out;
|
|
279
|
+
for (const line of content.slice(3, end).split('\n')) {
|
|
280
|
+
const m = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
281
|
+
if (m)
|
|
282
|
+
out[m[1]] = m[2].trim().replace(/^['"]|['"]$/g, '');
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
export function setFrontmatterStatus(path, status) {
|
|
287
|
+
const body = readFileSync(path, 'utf8');
|
|
288
|
+
if (/^status:/m.test(body)) {
|
|
289
|
+
writeFileSync(path, body.replace(/^status:.*$/m, `status: ${status}`), 'utf8');
|
|
290
|
+
}
|
|
291
|
+
else if (body.startsWith('---\n')) {
|
|
292
|
+
writeFileSync(path, body.replace('---\n', `---\nstatus: ${status}\n`), 'utf8');
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
writeFileSync(path, `---\nstatus: ${status}\n---\n\n${body}`, 'utf8');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Removes a request from the document it is about, once it has been done. A
|
|
300
|
+
* request that was accepted and written needs no record: the next reader sees
|
|
301
|
+
* the document as asked, and git keeps the history. Only a denial is kept —
|
|
302
|
+
* see `markRequestHandled` — because a denial is the one outcome the next
|
|
303
|
+
* writer cannot infer from the text.
|
|
304
|
+
*/
|
|
305
|
+
export function removeRequest(project, rel, from, reason) {
|
|
306
|
+
const path = docPath(project, rel);
|
|
307
|
+
if (!existsSync(path))
|
|
308
|
+
return;
|
|
309
|
+
const lines = readFileSync(path, 'utf8').split('\n');
|
|
310
|
+
let inSection = false;
|
|
311
|
+
for (let i = 0; i < lines.length; i++) {
|
|
312
|
+
const line = lines[i] ?? '';
|
|
313
|
+
const h = line.match(/^#{1,6}\s+(.*)$/);
|
|
314
|
+
if (h) {
|
|
315
|
+
inSection = CHANGE_REQUESTS.test((h[1] ?? '').trim());
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (!inSection)
|
|
319
|
+
continue;
|
|
320
|
+
const m = line.match(MARKED_LINE);
|
|
321
|
+
if (!m || !m[2])
|
|
322
|
+
continue;
|
|
323
|
+
if (m[3].replace(/^\.kortext\//, '') !== from.replace(/^.*\//, '') && m[3] !== from)
|
|
324
|
+
continue;
|
|
325
|
+
const [full, afterWrap] = foldWrapped(lines, i, m[4] ?? '');
|
|
326
|
+
if (full !== reason.trim())
|
|
327
|
+
continue;
|
|
328
|
+
let end = afterWrap;
|
|
329
|
+
while (end < lines.length && TRAILER.test(lines[end] ?? ''))
|
|
330
|
+
end++;
|
|
331
|
+
lines.splice(i, end - i);
|
|
332
|
+
writeFileSync(path, lines.join('\n'), 'utf8');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** Drops what this document asked of another, before it was sent. */
|
|
337
|
+
export function discardOutgoing(project, rel, target, reason) {
|
|
338
|
+
const path = docPath(project, rel);
|
|
339
|
+
if (!existsSync(path))
|
|
340
|
+
return;
|
|
341
|
+
const lines = readFileSync(path, 'utf8').split('\n');
|
|
342
|
+
let inSection = false;
|
|
343
|
+
for (let i = 0; i < lines.length; i++) {
|
|
344
|
+
const line = lines[i] ?? '';
|
|
345
|
+
const h = line.match(/^#{1,6}\s+(.*)$/);
|
|
346
|
+
if (h) {
|
|
347
|
+
inSection = CHANGE_REQUESTS.test((h[1] ?? '').trim());
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (!inSection)
|
|
351
|
+
continue;
|
|
352
|
+
const m = line.match(MARKED_LINE);
|
|
353
|
+
if (!m || m[2] || (m[1] ?? '').trim().toLowerCase() === 'x')
|
|
354
|
+
continue;
|
|
355
|
+
if (m[3].replace(/^\.kortext\//, '') !== target)
|
|
356
|
+
continue;
|
|
357
|
+
const [full, afterWrap] = foldWrapped(lines, i, m[4] ?? '');
|
|
358
|
+
if (full !== reason.trim())
|
|
359
|
+
continue;
|
|
360
|
+
lines.splice(i, afterWrap - i);
|
|
361
|
+
writeFileSync(path, lines.join('\n'), 'utf8');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Records a refusal in the document it is about: the request leaves the
|
|
367
|
+
* mailbox and goes into `## Decisions`, the reason under it. Not a request any
|
|
368
|
+
* more — a decision, which is why it carries no box, no "denied", no "prime"
|
|
369
|
+
* and no date: the heading is the word, prime is the only one who decides, and
|
|
370
|
+
* git keeps the day. The next agent to rewrite `rel` reads it here, and does
|
|
371
|
+
* not raise the same request again.
|
|
372
|
+
*/
|
|
373
|
+
export function markRequestHandled(project, rel, from, reason, outcome) {
|
|
374
|
+
removeRequest(project, rel, from, reason);
|
|
375
|
+
appendListItem(project, rel, 'Decisions', from, reason, outcome, false, false);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Adds one marked line under `heading`, creating the section when the document
|
|
379
|
+
* does not carry it. Sections are made on demand rather than shipped in the
|
|
380
|
+
* fifteen templates, so an empty one never exists to be mistaken for work.
|
|
381
|
+
*/
|
|
382
|
+
export function appendListItem(project, rel, heading, subject, reason, trailer, incoming = false, boxed = true) {
|
|
383
|
+
const path = docPath(project, rel);
|
|
384
|
+
if (!existsSync(path))
|
|
385
|
+
return;
|
|
386
|
+
const lines = readFileSync(path, 'utf8').split('\n');
|
|
387
|
+
const item = `- ${boxed ? '[ ] ' : ''}${incoming ? 'from ' : ''}\`${subject}\` — ${reason.replace(/\s+/g, ' ').trim()}`;
|
|
388
|
+
const block = trailer ? [item, ` - ${trailer}`] : [item];
|
|
389
|
+
const head = lines.findIndex((l) => {
|
|
390
|
+
const m = l.match(/^#{1,6}\s+(.*?)\s*$/);
|
|
391
|
+
return !!m && new RegExp(`^${heading}$`, 'i').test(m[1]);
|
|
392
|
+
});
|
|
393
|
+
if (head === -1) {
|
|
394
|
+
lines.push('', `## ${heading}`, '', ...block);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
let end = head + 1;
|
|
398
|
+
while (end < lines.length && !/^#{1,6}\s/.test(lines[end] ?? ''))
|
|
399
|
+
end++;
|
|
400
|
+
let at = end;
|
|
401
|
+
while (at > head + 1 && (lines[at - 1] ?? '').trim() === '')
|
|
402
|
+
at--;
|
|
403
|
+
lines.splice(at, 0, ...block);
|
|
404
|
+
}
|
|
405
|
+
writeFileSync(path, lines.join('\n'), 'utf8');
|
|
406
|
+
}
|
|
407
|
+
/** Files a request about `target`, made by `from`, where it will be decided. */
|
|
408
|
+
export function appendIncomingRequest(project, target, from, reason) {
|
|
409
|
+
appendListItem(project, target, 'Change Requests', from, reason, undefined, true);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Puts back what an agent's rewrite dropped. The lines `from` other documents
|
|
413
|
+
* are not the agent's to remove — an open one is a decision prime still owes,
|
|
414
|
+
* a ticked one is a decision prime made — and the prompt says so, but a prompt
|
|
415
|
+
* is not a guarantee. This is: after every agent write, any such line missing
|
|
416
|
+
* from the new text is appended again, state and outcome intact.
|
|
417
|
+
*/
|
|
418
|
+
export function restoreRequests(project, rel, priorText) {
|
|
419
|
+
if (priorText === null)
|
|
420
|
+
return;
|
|
421
|
+
const path = docPath(project, rel);
|
|
422
|
+
if (!existsSync(path))
|
|
423
|
+
return;
|
|
424
|
+
const written = readFileSync(path, 'utf8');
|
|
425
|
+
const missing = (heading, keep) => {
|
|
426
|
+
const kept = parseMarkedList(written, heading).filter(keep);
|
|
427
|
+
return parseMarkedList(priorText, heading)
|
|
428
|
+
.filter(keep)
|
|
429
|
+
.filter((r) => !kept.some((k) => k.subject === r.subject && k.reason === r.reason));
|
|
430
|
+
};
|
|
431
|
+
const line = (r, boxed) => [
|
|
432
|
+
`- ${boxed ? `[${r.settled ? 'x' : ' '}] ` : ''}${r.incoming ? 'from ' : ''}\`${r.subject}\` — ${r.reason}`,
|
|
433
|
+
...(r.outcome ? [` - ${r.outcome}`] : []),
|
|
434
|
+
];
|
|
435
|
+
const lines = written.split('\n');
|
|
436
|
+
const put = (heading, title, block) => {
|
|
437
|
+
if (block.length === 0)
|
|
438
|
+
return;
|
|
439
|
+
const head = lines.findIndex((l) => {
|
|
440
|
+
const m = l.match(/^#{1,6}\s+(.*?)\s*$/);
|
|
441
|
+
return !!m && heading.test(m[1]);
|
|
442
|
+
});
|
|
443
|
+
if (head === -1) {
|
|
444
|
+
lines.push('', `## ${title}`, '', ...block);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
let end = head + 1;
|
|
448
|
+
while (end < lines.length && !/^#{1,6}\s/.test(lines[end] ?? ''))
|
|
449
|
+
end++;
|
|
450
|
+
let at = end;
|
|
451
|
+
while (at > head + 1 && (lines[at - 1] ?? '').trim() === '')
|
|
452
|
+
at--;
|
|
453
|
+
lines.splice(at, 0, ...block);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
const requests = missing(CHANGE_REQUESTS, (r) => r.incoming).flatMap((r) => line(r, true));
|
|
457
|
+
const decisions = missing(DECISIONS, () => true).flatMap((r) => line(r, false));
|
|
458
|
+
if (requests.length === 0 && decisions.length === 0)
|
|
459
|
+
return;
|
|
460
|
+
put(CHANGE_REQUESTS, 'Change Requests', requests);
|
|
461
|
+
put(DECISIONS, 'Decisions', decisions);
|
|
462
|
+
writeFileSync(path, lines.join('\n'), 'utf8');
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Moves every open outgoing request of an approved document to the document it
|
|
466
|
+
* names. The agent writes what it asks of others into its own draft, where
|
|
467
|
+
* prime sees it before approving and may delete it; approval is the moment it
|
|
468
|
+
* becomes real, so this runs on approval — and once more on every listing, for
|
|
469
|
+
* documents approved before requests travelled. Idempotent: a moved line is
|
|
470
|
+
* gone from its source.
|
|
471
|
+
*/
|
|
472
|
+
export function deliverRequests(project, rel, only) {
|
|
473
|
+
const path = docPath(project, rel);
|
|
474
|
+
if (!existsSync(path))
|
|
475
|
+
return 0;
|
|
476
|
+
const text = readFileSync(path, 'utf8');
|
|
477
|
+
// Without a list this is the sweep for documents approved before requests
|
|
478
|
+
// travelled; with one it is prime pressing Send on a draft.
|
|
479
|
+
if (!only && readFrontmatter(text).status !== 'approved')
|
|
480
|
+
return 0;
|
|
481
|
+
const outgoing = parseOutgoing(text).filter((r) => {
|
|
482
|
+
if (only && !only.some((o) => o.target === r.target && o.reason === r.reason))
|
|
483
|
+
return false;
|
|
484
|
+
try {
|
|
485
|
+
return existsSync(docPath(project, r.target));
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
if (outgoing.length === 0)
|
|
492
|
+
return 0;
|
|
493
|
+
// Remove from the source first, line by line, then file in the target.
|
|
494
|
+
const lines = text.split('\n');
|
|
495
|
+
let inSection = false;
|
|
496
|
+
for (let i = 0; i < lines.length; i++) {
|
|
497
|
+
const line = lines[i] ?? '';
|
|
498
|
+
const h = line.match(/^#{1,6}\s+(.*)$/);
|
|
499
|
+
if (h) {
|
|
500
|
+
inSection = CHANGE_REQUESTS.test((h[1] ?? '').trim());
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (!inSection)
|
|
504
|
+
continue;
|
|
505
|
+
const m = line.match(MARKED_LINE);
|
|
506
|
+
if (!m || m[2] || (m[1] ?? '').trim().toLowerCase() === 'x')
|
|
507
|
+
continue;
|
|
508
|
+
const [reason, next] = foldWrapped(lines, i, m[4] ?? '');
|
|
509
|
+
const name = m[3].replace(/^\.kortext\//, '');
|
|
510
|
+
if (!outgoing.some((r) => r.target === name && r.reason === reason))
|
|
511
|
+
continue;
|
|
512
|
+
lines.splice(i, next - i);
|
|
513
|
+
i--;
|
|
514
|
+
}
|
|
515
|
+
writeFileSync(path, lines.join('\n'), 'utf8');
|
|
516
|
+
for (const r of outgoing)
|
|
517
|
+
appendIncomingRequest(project, r.target, rel, r.reason);
|
|
518
|
+
return outgoing.length;
|
|
519
|
+
}
|
|
520
|
+
export function listDocs(db, project, pkgRoot) {
|
|
521
|
+
const map = loadDocMap(pkgRoot, project.kind ?? 'new');
|
|
522
|
+
const statuses = new Map();
|
|
523
|
+
const docs = [];
|
|
524
|
+
const dir = join(project.repo_path, '.kortext');
|
|
525
|
+
if (existsSync(dir)) {
|
|
526
|
+
// Requests travel on approval. A document approved before they did still
|
|
527
|
+
// carries its outgoing lines; deliver those now, before anything is read.
|
|
528
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
|
|
529
|
+
deliverRequests(project, file);
|
|
530
|
+
}
|
|
531
|
+
// One shelf: every .md in .kortext/ is a document of this project.
|
|
532
|
+
for (const file of readdirSync(dir)
|
|
533
|
+
.filter((f) => f.endsWith('.md'))
|
|
534
|
+
.sort()) {
|
|
535
|
+
const rel = file;
|
|
536
|
+
const body = readFileSync(join(dir, file), 'utf8');
|
|
537
|
+
const fm = readFrontmatter(body);
|
|
538
|
+
const status = fm.status ?? 'uninitialized';
|
|
539
|
+
statuses.set(rel, status);
|
|
540
|
+
docs.push({
|
|
541
|
+
rel,
|
|
542
|
+
name: file.replace(/\.md$/, ''),
|
|
543
|
+
status,
|
|
544
|
+
author: fm.author ?? map.get(rel)?.author ?? null,
|
|
545
|
+
inputs: map.get(rel)?.inputs ?? [],
|
|
546
|
+
blocked: false,
|
|
547
|
+
dependentOn: [],
|
|
548
|
+
openQuestions: status !== 'uninitialized' && hasOpenQuestions(body),
|
|
549
|
+
hasProducingStep: map.has(rel),
|
|
550
|
+
// Read from the document's own section: what others asked of it is
|
|
551
|
+
// filed here when they are approved, and decided here. An unwritten
|
|
552
|
+
// document can already hold some — they go into its first write.
|
|
553
|
+
revisionRequests: parseIncoming(body),
|
|
554
|
+
denied: parseDenied(body),
|
|
555
|
+
outgoing: status === 'uninitialized' ? [] : parseOutgoing(body),
|
|
556
|
+
warnings: status === 'uninitialized' ? [] : parseWarnings(body),
|
|
557
|
+
conflicts: status === 'uninitialized' ? [] : parseConflicts(body),
|
|
558
|
+
// Filled once every document is known; nothing can be filed before then.
|
|
559
|
+
section: 'todo',
|
|
560
|
+
state: 'waiting',
|
|
561
|
+
detail: 'queue',
|
|
562
|
+
pendingRecheck: false,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// 'not-applicable' satisfies a dependency: the doc was considered and
|
|
567
|
+
// deliberately skipped — downstream steps must not wait on it.
|
|
568
|
+
const settled = (s) => s === 'approved' || s === 'not-applicable';
|
|
569
|
+
const byRel = new Map(docs.map((d) => [d.rel, d]));
|
|
570
|
+
for (const doc of docs) {
|
|
571
|
+
doc.blocked = doc.inputs.some((i) => !settled(statuses.get(i)));
|
|
572
|
+
// Only approved readers need rechecking when an input becomes unsettled or receives a request.
|
|
573
|
+
if (doc.status !== 'approved')
|
|
574
|
+
continue;
|
|
575
|
+
doc.dependentOn = doc.inputs.filter((i) => {
|
|
576
|
+
const input = byRel.get(i);
|
|
577
|
+
if (!input)
|
|
578
|
+
return false;
|
|
579
|
+
return input.revisionRequests.length > 0 || !settled(input.status);
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
// Sort by maximum dependency depth. Memoize per document, but keep cycle detection
|
|
583
|
+
// local to each traversal path so shared inputs in a diamond retain their depth.
|
|
584
|
+
const memo = new Map();
|
|
585
|
+
const depth = (rel, path = new Set()) => {
|
|
586
|
+
const done = memo.get(rel);
|
|
587
|
+
if (done !== undefined)
|
|
588
|
+
return done;
|
|
589
|
+
if (path.has(rel))
|
|
590
|
+
return 0; // ponytail: the shipped workflows are acyclic; this is for hand-edited ones
|
|
591
|
+
path.add(rel);
|
|
592
|
+
const ins = map.get(rel)?.inputs ?? [];
|
|
593
|
+
const d = ins.length === 0 ? 0 : 1 + Math.max(...ins.map((i) => depth(i, path)));
|
|
594
|
+
path.delete(rel);
|
|
595
|
+
memo.set(rel, d);
|
|
596
|
+
return d;
|
|
597
|
+
};
|
|
598
|
+
// The last job per document, and the rechecks queued against it. Both live in
|
|
599
|
+
// the database only; the runner cannot be imported here — it imports this file.
|
|
600
|
+
const jobs = new Map();
|
|
601
|
+
for (const row of db
|
|
602
|
+
.prepare(`SELECT doc_rel, kind, status, notes FROM jobs
|
|
603
|
+
WHERE project_id = ? AND id IN (SELECT MAX(id) FROM jobs WHERE project_id = ? GROUP BY doc_rel)`)
|
|
604
|
+
.all(project.id, project.id)) {
|
|
605
|
+
let isUpdate = false;
|
|
606
|
+
try {
|
|
607
|
+
isUpdate = JSON.parse(row.notes || '[]').length > 0;
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
isUpdate = false;
|
|
611
|
+
}
|
|
612
|
+
jobs.set(row.doc_rel, { kind: row.kind, status: row.status, isUpdate });
|
|
613
|
+
}
|
|
614
|
+
const rechecking = new Set(db
|
|
615
|
+
.prepare('SELECT DISTINCT reader_rel FROM pending_rechecks WHERE project_id = ?')
|
|
616
|
+
.all(project.id).map((r) => r.reader_rel));
|
|
617
|
+
for (const doc of docs) {
|
|
618
|
+
const job = jobs.get(doc.rel) ?? null;
|
|
619
|
+
doc.pendingRecheck =
|
|
620
|
+
rechecking.has(doc.rel) || (job?.kind === 'recheck' && job.status === 'running');
|
|
621
|
+
Object.assign(doc, fileDoc(doc, job));
|
|
622
|
+
}
|
|
623
|
+
docs.sort((a, b) => depth(a.rel) - depth(b.rel) || a.rel.localeCompare(b.rel));
|
|
624
|
+
return docs;
|
|
625
|
+
}
|
|
626
|
+
// The handshake is done when every document the workflow produces is settled
|
|
627
|
+
// (approved or not-applicable). Docs without a producing step (unmapped
|
|
628
|
+
// skeletons a project already carried) don't gate completion.
|
|
629
|
+
export function analysisComplete(db, project, pkgRoot) {
|
|
630
|
+
if (db.prepare('SELECT 1 FROM pending_rechecks WHERE project_id = ? LIMIT 1').get(project.id)) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
if (db
|
|
634
|
+
.prepare("SELECT 1 FROM jobs WHERE project_id = ? AND status = 'running' AND kind != 'plan' LIMIT 1")
|
|
635
|
+
.get(project.id)) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
const map = loadDocMap(pkgRoot, project.kind ?? 'new');
|
|
639
|
+
const docs = listDocs(db, project, pkgRoot);
|
|
640
|
+
const byRel = new Map(docs.map((d) => [d.rel, d.status]));
|
|
641
|
+
const targets = [...map.keys()];
|
|
642
|
+
if (targets.length === 0)
|
|
643
|
+
return false;
|
|
644
|
+
const settled = (s) => s === 'approved' || s === 'not-applicable';
|
|
645
|
+
// The brief gates the new-project flow even though no step produces it
|
|
646
|
+
if ((project.kind ?? 'new') === 'new' && !settled(byRel.get('BRIEF.md')))
|
|
647
|
+
return false;
|
|
648
|
+
if (targets.some((rel) => docs.find((d) => d.rel === rel)?.openQuestions))
|
|
649
|
+
return false;
|
|
650
|
+
if (docs.some((d) => d.revisionRequests.length > 0))
|
|
651
|
+
return false;
|
|
652
|
+
// Conflicts and findings do not gate the handshake. A conflict is not a
|
|
653
|
+
// decision prime avoided; it is one deferred to the moment there is enough
|
|
654
|
+
// information — the build phase, where prime is present anyway. It is handed
|
|
655
|
+
// over, not settled here.
|
|
656
|
+
return targets.every((rel) => settled(byRel.get(rel)));
|
|
657
|
+
}
|
|
658
|
+
// rel names a document on the shelf ("STACK.md"). The pattern forbids traversal
|
|
659
|
+
// ("." never starts the name) and anything outside .kortext/ itself.
|
|
660
|
+
export function docPath(project, rel) {
|
|
661
|
+
if (!/^[A-Za-z][\w.-]*\.md$/.test(rel)) {
|
|
662
|
+
throw new Error(`bad doc path: ${rel}`);
|
|
663
|
+
}
|
|
664
|
+
return join(project.repo_path, '.kortext', rel);
|
|
665
|
+
}
|
|
666
|
+
export function docVersion(content) {
|
|
667
|
+
return createHash('sha256').update(content).digest('hex');
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Keeps what a document said, so a revision can be read as a change rather than
|
|
671
|
+
* as a new text. Only the two paths that rewrite the prose record: the agent's
|
|
672
|
+
* write and prime's save. Approving, ticking a demand and appending a recheck's
|
|
673
|
+
* line all touch one line the reader already strips.
|
|
674
|
+
*
|
|
675
|
+
* `priorText` bootstraps the chain: the first recorded write has no predecessor
|
|
676
|
+
* to compare against, so the bytes it replaced go in first.
|
|
677
|
+
*/
|
|
678
|
+
export function recordVersion(db, project, rel, content, source, priorText, jobId) {
|
|
679
|
+
const insert = db.prepare('INSERT INTO doc_versions (project_id, rel, sha, content, source, job_id) VALUES (?, ?, ?, ?, ?, ?)');
|
|
680
|
+
const seen = db
|
|
681
|
+
.prepare('SELECT 1 FROM doc_versions WHERE project_id = ? AND rel = ? LIMIT 1')
|
|
682
|
+
.get(project.id, rel);
|
|
683
|
+
if (!seen && priorText !== null) {
|
|
684
|
+
insert.run(project.id, rel, docVersion(priorText), priorText, 'pre-existing', null);
|
|
685
|
+
}
|
|
686
|
+
insert.run(project.id, rel, docVersion(content), content, source, jobId ?? null);
|
|
687
|
+
}
|
|
688
|
+
/** Everything above and including the closing `---` of the frontmatter. */
|
|
689
|
+
function stripFrontmatter(content) {
|
|
690
|
+
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
|
691
|
+
}
|
|
692
|
+
export function listVersions(db, project, rel) {
|
|
693
|
+
return db
|
|
694
|
+
.prepare('SELECT id, sha, content, source, created_at FROM doc_versions WHERE project_id = ? AND rel = ? ORDER BY id DESC')
|
|
695
|
+
.all(project.id, rel).map(({ content, ...v }) => ({ ...v, bodySha: docVersion(stripFrontmatter(content)) }));
|
|
696
|
+
}
|
|
697
|
+
export function readVersion(db, project, id) {
|
|
698
|
+
return (db
|
|
699
|
+
.prepare('SELECT content, rel FROM doc_versions WHERE id = ? AND project_id = ?')
|
|
700
|
+
.get(id, project.id) ?? null);
|
|
701
|
+
}
|