cohorte 2.10.0 → 3.0.0-dev.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.
Files changed (211) hide show
  1. package/README.md +7 -0
  2. package/assets/manifest.json +193 -0
  3. package/assets/migrations/config/README.md +12 -0
  4. package/assets/migrations/state/0001_init.sql +154 -0
  5. package/assets/prompts/README.md +11 -0
  6. package/assets/prompts/agents/fixer.md +7 -0
  7. package/assets/prompts/agents/implementer.md +12 -0
  8. package/assets/prompts/agents/reviewer.md +8 -0
  9. package/assets/prompts/agents/security-reviewer.md +7 -0
  10. package/assets/prompts/phases/build.md +5 -0
  11. package/assets/prompts/phases/review.md +5 -0
  12. package/assets/prompts/phases/test.md +5 -0
  13. package/assets/prompts/system.md +11 -0
  14. package/assets/schemas/agent-output.schema.json +225 -0
  15. package/assets/schemas/auth-status.schema.json +87 -0
  16. package/assets/schemas/command-result.schema.json +192 -0
  17. package/assets/schemas/commands.schema.json +952 -0
  18. package/assets/schemas/config.schema.json +2259 -0
  19. package/assets/schemas/doctor-report.schema.json +104 -0
  20. package/assets/schemas/events.schema.json +8514 -0
  21. package/assets/schemas/fake-script.schema.json +549 -0
  22. package/assets/schemas/inspect.schema.json +3133 -0
  23. package/assets/schemas/manifest.schema.json +79 -0
  24. package/assets/schemas/ownership.schema.json +52 -0
  25. package/assets/schemas/policy-verdict.schema.json +282 -0
  26. package/assets/schemas/project-model.schema.json +783 -0
  27. package/assets/schemas/project-status.schema.json +217 -0
  28. package/assets/schemas/reconcile-plan.schema.json +155 -0
  29. package/assets/schemas/run-diff.schema.json +177 -0
  30. package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
  31. package/assets/schemas/run-state.schema.json +1755 -0
  32. package/assets/schemas/runtime-capabilities.schema.json +1545 -0
  33. package/assets/schemas/sandbox-capabilities.schema.json +116 -0
  34. package/assets/schemas/skill.schema.json +87 -0
  35. package/assets/schemas/spec.schema.json +202 -0
  36. package/assets/schemas/tool-catalogue.schema.json +655 -0
  37. package/assets/schemas/trust-record.schema.json +38 -0
  38. package/assets/skills/README.md +9 -0
  39. package/assets/skills/inspect-surface.md +4 -0
  40. package/dist/agent-host.mjs +50 -0
  41. package/dist/agent-host.mjs.map +1 -0
  42. package/dist/bundle-manifest.json +102 -0
  43. package/dist/chunks/__host-DgGPyco9.mjs +18 -0
  44. package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
  45. package/dist/chunks/approve-Cv135UqI.mjs +19 -0
  46. package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
  47. package/dist/chunks/auth-BT5oVp76.mjs +132 -0
  48. package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
  49. package/dist/chunks/auth-D8rCVY33.mjs +84 -0
  50. package/dist/chunks/auth-D8rCVY33.mjs.map +1 -0
  51. package/dist/chunks/brainstorm-CU10T1tm.mjs +7 -0
  52. package/dist/chunks/brainstorm-CU10T1tm.mjs.map +1 -0
  53. package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
  54. package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
  55. package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
  56. package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
  57. package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
  58. package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
  59. package/dist/chunks/compose-Cr0hP0qm.mjs +7303 -0
  60. package/dist/chunks/compose-Cr0hP0qm.mjs.map +1 -0
  61. package/dist/chunks/config-DefHA2Lw.mjs +96 -0
  62. package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
  63. package/dist/chunks/contract-B0ra2-bm.mjs +295 -0
  64. package/dist/chunks/contract-B0ra2-bm.mjs.map +1 -0
  65. package/dist/chunks/contract-vYbwYr0Y.mjs +123 -0
  66. package/dist/chunks/contract-vYbwYr0Y.mjs.map +1 -0
  67. package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
  68. package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
  69. package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
  70. package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
  71. package/dist/chunks/discover-30Ciya_0.mjs +20 -0
  72. package/dist/chunks/discover-30Ciya_0.mjs.map +1 -0
  73. package/dist/chunks/doctor-DfCrKpfT.mjs +222 -0
  74. package/dist/chunks/doctor-DfCrKpfT.mjs.map +1 -0
  75. package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
  76. package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
  77. package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
  78. package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
  79. package/dist/chunks/init-Okz4mlhW.mjs +30 -0
  80. package/dist/chunks/init-Okz4mlhW.mjs.map +1 -0
  81. package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
  82. package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
  83. package/dist/chunks/logs-Giq5sUau.mjs +25 -0
  84. package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
  85. package/dist/chunks/migrate-DRoFUg53.mjs +19 -0
  86. package/dist/chunks/migrate-DRoFUg53.mjs.map +1 -0
  87. package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
  88. package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
  89. package/dist/chunks/pause-BASBfKLO.mjs +16 -0
  90. package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
  91. package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
  92. package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
  93. package/dist/chunks/providers-Dx78imKj.mjs +19 -0
  94. package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
  95. package/dist/chunks/reconcile-BfD785gd.mjs +26 -0
  96. package/dist/chunks/reconcile-BfD785gd.mjs.map +1 -0
  97. package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
  98. package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
  99. package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
  100. package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
  101. package/dist/chunks/retry-CLrUem1H.mjs +15 -0
  102. package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
  103. package/dist/chunks/review-BDdjb6kB.mjs +17 -0
  104. package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
  105. package/dist/chunks/run-CgTWYlcd.mjs +82 -0
  106. package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
  107. package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
  108. package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
  109. package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
  110. package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
  111. package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
  112. package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
  113. package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
  114. package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
  115. package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
  116. package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
  117. package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
  118. package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
  119. package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
  120. package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
  121. package/dist/chunks/src-CId6kMsY.mjs +1689 -0
  122. package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
  123. package/dist/chunks/src-CfAOde7C.mjs +5403 -0
  124. package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
  125. package/dist/chunks/src-Cu3TxNob.mjs +783 -0
  126. package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
  127. package/dist/chunks/src-DcU4IXMe.mjs +117 -0
  128. package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
  129. package/dist/chunks/src-JxawBQBn.mjs +446 -0
  130. package/dist/chunks/src-JxawBQBn.mjs.map +1 -0
  131. package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
  132. package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
  133. package/dist/chunks/status-SMjiOMYy.mjs +29 -0
  134. package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
  135. package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
  136. package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
  137. package/dist/chunks/update-BXIppsbi.mjs +16 -0
  138. package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
  139. package/dist/cli.mjs +175 -0
  140. package/dist/cli.mjs.map +1 -0
  141. package/package.json +17 -32
  142. package/CHANGELOG.md +0 -1453
  143. package/bin/cli.js +0 -812
  144. package/bin/report.js +0 -141
  145. package/core/adapter/render.js +0 -389
  146. package/core/agents/implementer.template.md +0 -142
  147. package/core/agents/profile-reader.md +0 -50
  148. package/core/agents/release.md +0 -58
  149. package/core/agents/review.md +0 -179
  150. package/core/commands/cohorte-align-ds.md +0 -33
  151. package/core/commands/cohorte-audit.md +0 -58
  152. package/core/commands/cohorte-brainstorm.md +0 -70
  153. package/core/commands/cohorte-build.md +0 -219
  154. package/core/commands/cohorte-doctor.md +0 -170
  155. package/core/commands/cohorte-fix.md +0 -80
  156. package/core/commands/cohorte-fleet.md +0 -103
  157. package/core/commands/cohorte-init-pipeline.md +0 -31
  158. package/core/commands/cohorte-intake.md +0 -92
  159. package/core/commands/cohorte-patch.md +0 -118
  160. package/core/commands/cohorte-refactor.md +0 -59
  161. package/core/commands/cohorte-retro.md +0 -85
  162. package/core/commands/cohorte-review.md +0 -270
  163. package/core/commands/cohorte-ship.md +0 -122
  164. package/core/commands/cohorte-spec.md +0 -101
  165. package/core/commands/cohorte-update-pipeline.md +0 -134
  166. package/core/hooks/gate.py +0 -580
  167. package/core/runtimes/claude.json +0 -74
  168. package/core/runtimes/codex.json +0 -83
  169. package/core/runtimes/cursor.json +0 -76
  170. package/core/runtimes/gemini.json +0 -76
  171. package/core/runtimes/opencode.json +0 -73
  172. package/core/templates/agent-handoff.md +0 -34
  173. package/core/templates/brainstorm-return.md +0 -36
  174. package/core/templates/decisions.template.md +0 -42
  175. package/core/templates/design-brief.md +0 -44
  176. package/core/templates/patch.template.md +0 -86
  177. package/core/templates/pr-body.md +0 -25
  178. package/core/templates/review-feedback.md +0 -38
  179. package/core/templates/spec.template.md +0 -87
  180. package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
  181. package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
  182. package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
  183. package/core/templates/steps/init-pipeline/04-write-render.md +0 -115
  184. package/core/templates/steps/init-pipeline/05-report.md +0 -12
  185. package/core/workflows/audit.js +0 -240
  186. package/core/workflows/loop.js +0 -641
  187. package/core/workflows/refactor.js +0 -276
  188. package/core/workflows/review.js +0 -456
  189. package/install.ps1 +0 -103
  190. package/install.sh +0 -114
  191. package/lib/doctor.js +0 -504
  192. package/lib/runtime.js +0 -134
  193. package/lib/versions.js +0 -149
  194. package/lib/yaml.js +0 -129
  195. package/profile/PIPELINE.template.md +0 -251
  196. package/profile/SCHEMA.md +0 -698
  197. package/profile/cohorte.config.template.yaml +0 -43
  198. package/scripts/assert-gate-hook.mjs +0 -75
  199. package/scripts/kanban-move.sh +0 -313
  200. package/scripts/metrics/collect.mjs +0 -531
  201. package/scripts/metrics/prices.json +0 -42
  202. package/scripts/new-feature.sh.template +0 -98
  203. package/scripts/preflight.sh +0 -124
  204. package/scripts/remove-feature.sh.template +0 -56
  205. package/scripts/test-adapter.mjs +0 -368
  206. package/scripts/test-gate.mjs +0 -428
  207. package/scripts/test-kanban.mjs +0 -304
  208. package/scripts/test-lib.mjs +0 -310
  209. package/scripts/test-metrics.mjs +0 -156
  210. package/scripts/test-workflows.mjs +0 -758
  211. package/scripts/validate-core.mjs +0 -320
@@ -1,43 +0,0 @@
1
- # ~/.claude/cohorte.config.yaml — GLOBAL, user-scoped config for the Cohorte
2
- # pipeline capabilities. One machine, every project. NOT tied to any project's PIPELINE.md.
3
- #
4
- # Seeded once by the installer (cohorte install) and never clobbered on update. Wired
5
- # interactively by /cohorte-init-pipeline (new project) and /cohorte-update-pipeline (existing project) —
6
- # you should never need to hand-edit it.
7
- #
8
- # The `# cfg:<key>` markers below are anchors the installer's interactive prompts write to;
9
- # leave them in place.
10
-
11
- enabled: true # cfg:enabled — master switch; false disables every capability below
12
-
13
- # ── obsidian ──────────────────────────────────────────────────────────────────
14
- # Local vault used by the kanban mirror — defined here ONCE so no capability ever duplicates
15
- # the path. Absolute path to the folder you open in Obsidian.
16
- obsidian:
17
- vault_path: "" # cfg:vault_path
18
-
19
- # ── kanban (dev pipeline mirror) ─────────────────────────────────────────────
20
- # Mirrors /cohorte-brainstorm…/cohorte-ship onto an Obsidian Kanban board — one board per project. The
21
- # board path points at your PERSONAL vault, so it is machine-specific and lives here (never
22
- # committed in a repo's PIPELINE.md). /cohorte-init-pipeline + /cohorte-update-pipeline write `boards` for you.
23
- # See SCHEMA.md §Kanban.
24
- kanban:
25
- enabled: false # cfg:kanban_enabled — master switch for the kanban mirror
26
- # Default column mapping: pipeline stage → the EXACT board column heading. Shared by every
27
- # board; a board may override its own `columns` inside boards[] below.
28
- columns:
29
- ideas: "Ideas"
30
- brainstorm: "Brainstorm"
31
- spec: "Spec"
32
- ready: "Ready to build"
33
- building: "Building"
34
- review: "Review"
35
- fix: "Fix"
36
- ship: "Ship"
37
- shipped: "Shipped"
38
- # One entry per project, keyed by the project's PIPELINE.md `name` (e.g. SAMO). `board` is a
39
- # path RELATIVE to obsidian.vault_path. Written by /cohorte-init-pipeline + /cohorte-update-pipeline.
40
- boards: {}
41
- # SAMO:
42
- # board: "Samo/Tasks.md"
43
- # # columns: { ideas: "Idées", ... } # optional per-board override of the defaults above
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env node
2
- //
3
- // assert-gate-hook.mjs — post-install assertion on a settings.json's gate hook.
4
- //
5
- // node scripts/assert-gate-hook.mjs <path-to-settings.json>
6
- //
7
- // Two invariants, both regressions that shipped to users:
8
- //
9
- // 1. EXACTLY ONE registration. Until 1.3.2 the installers appended
10
- // if-absent, and their "already registered?" test was a bare
11
- // `.endsWith("gate.py")` — false for the Windows form `py "C:\...\gate.py"`
12
- // because of the trailing quote. Every re-install appended another copy
13
- // (four seen in the wild), so gate.py ran once per copy on every Bash call.
14
- // CI installed only once, so it never noticed. Callers must install TWICE
15
- // before running this.
16
- //
17
- // 2. THE MATCHER COVERS Task. gate.py's preflight phase gate dispatches on
18
- // tool_name === "Task" (the `preflight` block in a repo's
19
- // gate-config.json). A Bash-only matcher never delivers a Task call to the
20
- // hook, so the gate was dead code from 1.3.0 (which introduced it) to 1.3.1.
21
- //
22
- // Exits non-zero with a specific message on failure.
23
-
24
- import fs from 'node:fs';
25
-
26
- const settingsPath = process.argv[2];
27
- if (!settingsPath) {
28
- console.error('usage: assert-gate-hook.mjs <path-to-settings.json>');
29
- process.exit(2);
30
- }
31
-
32
- let data;
33
- try {
34
- data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
35
- } catch (err) {
36
- console.error(`assert-gate-hook: cannot read ${settingsPath}: ${err.message}`);
37
- process.exit(1);
38
- }
39
-
40
- const pre = (data.hooks || {}).PreToolUse || [];
41
- const isGate = (entry) =>
42
- (entry.hooks || []).some(
43
- (h) => typeof h.command === 'string' && h.command.trim().replace(/"+$/, '').endsWith('gate.py')
44
- );
45
- const gate = pre.filter(isGate);
46
-
47
- if (gate.length !== 1) {
48
- console.error(
49
- `assert-gate-hook: expected exactly 1 gate.py registration, found ${gate.length}.\n` +
50
- (gate.length > 1
51
- ? ' Registration is not idempotent — a re-install duplicated the hook.'
52
- : ' The installer did not register the gate hook.') +
53
- `\n PreToolUse: ${JSON.stringify(pre, null, 2)}`
54
- );
55
- process.exit(1);
56
- }
57
-
58
- const matcher = gate[0].matcher || '';
59
- if (!/\bTask\b/.test(matcher)) {
60
- console.error(
61
- `assert-gate-hook: gate matcher must cover Task, got ${JSON.stringify(matcher)}.\n` +
62
- " gate.py's preflight phase gate keys off tool_name === 'Task'; without it\n" +
63
- ' the `preflight` block in gate-config.json is dead code.'
64
- );
65
- process.exit(1);
66
- }
67
- if (!/\bBash\b/.test(matcher)) {
68
- console.error(
69
- `assert-gate-hook: gate matcher must cover Bash, got ${JSON.stringify(matcher)}.\n` +
70
- ' The deny/ask command gating runs on Bash tool calls.'
71
- );
72
- process.exit(1);
73
- }
74
-
75
- console.log(`gate hook ok — 1 registration, matcher ${JSON.stringify(matcher)}`);
@@ -1,313 +0,0 @@
1
- #!/bin/sh
2
- # kanban-move.sh — move a feature card on an Obsidian Kanban board without
3
- # loading the board into an agent's context (SCHEMA.md §Kanban "Move a card").
4
- #
5
- # kanban-move.sh <board.md|auto> <feature-id> <column|stage> [options]
6
- # kanban-move.sh --check resolve only: print the board this
7
- # project maps to, or why it does not
8
- #
9
- # --pr <num> ensure the card line ends with ` — PR #<num>`
10
- # --title <title> text to use when the card has to be created
11
- # --project <name> profile name to look up in kanban.boards (default: read
12
- # `name:` from ./PIPELINE.md)
13
- # --profile <path> where to read that name from (default: ./PIPELINE.md)
14
- #
15
- # `auto` as the board resolves the board FROM THE CONFIG — vault path, this
16
- # project's board file, and the stage→heading mapping — instead of making the
17
- # caller do it. This exists because the callers are agents: told only "no-op
18
- # silently if no board", a fresh session (every phase runs after a /clear)
19
- # concluded "no board is configured" without ever opening the config, and the
20
- # card silently stopped moving mid-pipeline. Resolution is not a judgment call,
21
- # so it is not left to one. When nothing resolves the script says which link is
22
- # missing on stdout and exits 0 — a no-op you can read, never one you infer.
23
- #
24
- # Behavior of the move itself (mirrors the documented manual op):
25
- # - finds the list item tagged #<feature-id> (plus its indented sub-notes),
26
- # removes it from its current column, appends it under `## <target-column>`
27
- # - no card yet ⇒ creates `- [ ] <title|id> #<id>` in the target column
28
- # - duplicates ⇒ keeps the first, drops the rest
29
- # - never touches the front-matter or the trailing `%% kanban:settings %%` block
30
- #
31
- # Exit codes: 0 ok (incl. "kanban not configured") · 2 usage · 3 board/column
32
- # not found. A configured board that cannot be moved is loud; an unconfigured
33
- # one is quiet — the pipeline never blocks on the board.
34
- set -eu
35
-
36
- usage='usage: kanban-move.sh <board.md|auto> <feature-id> <column|stage> [--pr <num>] [--title <title>] [--project <name>] [--profile <path>]
37
- kanban-move.sh --check [--project <name>] [--profile <path>]'
38
- check_only=false
39
- if [ "${1-}" = --check ]; then
40
- check_only=true; board=auto; id=""; col=""; shift
41
- else
42
- [ $# -ge 3 ] || { echo "$usage" >&2; exit 2; }
43
- board="$1"; id="$2"; col="$3"; shift 3
44
- fi
45
- pr=""; title=""; project=""; profile="PIPELINE.md"
46
- while [ $# -gt 0 ]; do
47
- # A flag with no value must exit 2 (usage) like every other usage error — a bare
48
- # `shift 2` fails under set -e with exit 1, outside the 0/2/3 contract callers branch on.
49
- case "$1" in
50
- --pr|--title|--project|--profile)
51
- [ $# -ge 2 ] || { echo "error: $1 needs a value" >&2; echo "$usage" >&2; exit 2; } ;;
52
- esac
53
- case "$1" in
54
- --pr) pr="$2"; shift 2 ;;
55
- --title) title="$2"; shift 2 ;;
56
- --project) project="$2"; shift 2 ;;
57
- --profile) profile="$2"; shift 2 ;;
58
- *) echo "error: unknown flag $1" >&2; echo "$usage" >&2; exit 2 ;;
59
- esac
60
- done
61
-
62
- TAB=$(printf '\t')
63
- # One config per human, wherever the runtime that installed it put it: `~/.claude` for a
64
- # Claude Code install (historical, still authoritative), `~/.cohorte` for every other coding
65
- # agent. Probe both — a repo driven from two agents must resolve ONE board, not two.
66
- CONFIG="${COHORTE_CONFIG:-}"
67
- if [ -z "$CONFIG" ]; then
68
- for c in "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/cohorte.config.yaml" \
69
- "$HOME/.cohorte/cohorte.config.yaml"; do
70
- [ -f "$c" ] && { CONFIG="$c"; break; }
71
- done
72
- CONFIG="${CONFIG:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/cohorte.config.yaml}"
73
- fi
74
-
75
- # The nine pipeline stages and their default headings. A stage key given as the
76
- # target column is mapped through the config (per-board `columns` first, then the
77
- # global `kanban.columns`) and falls back to these — so a stage key works even
78
- # with an explicit board path and no config at all. Anything not in this list is
79
- # taken as a literal heading, which keeps hand-written columns working.
80
- default_heading() {
81
- case "$1" in
82
- ideas) echo "Ideas" ;;
83
- brainstorm) echo "Brainstorm" ;;
84
- spec) echo "Spec" ;;
85
- ready) echo "Ready to build" ;;
86
- building) echo "Building" ;;
87
- review) echo "Review" ;;
88
- fix) echo "Fix" ;;
89
- ship) echo "Ship" ;;
90
- shipped) echo "Shipped" ;;
91
- *) echo "" ;;
92
- esac
93
- }
94
-
95
- # Read `name:` from the profile's `yaml pipeline-profile` block. Only the block —
96
- # a `name:` under some other key (a surface, a persona) is not the project name.
97
- read_project_name() {
98
- [ -f "$1" ] || return 0
99
- awk '
100
- /^```[ \t]*yaml[ \t]+pipeline-profile/ { inblock = 1; next }
101
- /^```/ { if (inblock) exit }
102
- inblock && /^name:[ \t]/ {
103
- v = $0; sub(/^name:[ \t]*/, "", v)
104
- sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v)
105
- gsub(/^["\047]|["\047]$/, "", v)
106
- print v; exit
107
- }
108
- ' "$1"
109
- }
110
-
111
- # Minimal YAML reader for the fixed shape the installer writes. Not a general
112
- # parser: it walks an indent stack and emits only the handful of paths the kanban
113
- # mirror needs, resolved against the project name we were given (so a name with a
114
- # dot in it still matches — the comparison is on whole strings, not a joined key).
115
- read_config() {
116
- PROJ="$1" awk '
117
- function clean(v, c, q, i, ch, out) {
118
- sub(/^[ \t]+/, "", v)
119
- if (v == "") return ""
120
- c = substr(v, 1, 1)
121
- if (c == "\"" || c == "\047") {
122
- q = c; out = ""
123
- for (i = 2; i <= length(v); i++) {
124
- ch = substr(v, i, 1)
125
- if (ch == q) break
126
- out = out ch
127
- }
128
- return out
129
- }
130
- sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v)
131
- return v
132
- }
133
- # `columns: { ideas: "Idées", ready: "Prêt" }` — the flow form the config
134
- # template documents for a per-board override.
135
- function flow(path, v, n, i, parts, p, k) {
136
- sub(/^\{[ \t]*/, "", v); sub(/[ \t]*\}[ \t]*$/, "", v)
137
- n = split(v, parts, ",")
138
- for (i = 1; i <= n; i++) {
139
- p = index(parts[i], ":")
140
- if (p == 0) continue
141
- k = substr(parts[i], 1, p - 1)
142
- gsub(/^[ \t]+|[ \t]+$/, "", k)
143
- emit(path "." k, clean(substr(parts[i], p + 1)))
144
- }
145
- }
146
- function emit(path, v, stage) {
147
- if (v == "") return
148
- if (path == "enabled") { print "ENABLED\t" v; return }
149
- if (path == "kanban.enabled") { print "KENABLED\t" v; return }
150
- if (path == "obsidian.vault_path") { print "VAULT\t" v; return }
151
- if (path == "kanban.boards." proj ".board") { print "BOARD\t" v; return }
152
- if (index(path, "kanban.columns.") == 1) {
153
- print "COL." substr(path, length("kanban.columns.") + 1) "\t" v; return
154
- }
155
- if (index(path, "kanban.boards." proj ".columns.") == 1) {
156
- print "BCOL." substr(path, length("kanban.boards." proj ".columns.") + 1) "\t" v; return
157
- }
158
- }
159
- BEGIN { proj = ENVIRON["PROJ"]; top = 0 }
160
- {
161
- line = $0; sub(/\r$/, "", line)
162
- if (line ~ /^[ \t]*$/ || line ~ /^[ \t]*#/) next
163
- match(line, /^[ ]*/); ind = RLENGTH
164
- rest = substr(line, ind + 1)
165
- if (rest ~ /^- /) next
166
- p = index(rest, ":")
167
- if (p == 0) next
168
- key = substr(rest, 1, p - 1)
169
- if (key ~ /[ \t]/) next
170
- raw = substr(rest, p + 1)
171
- while (top > 0 && indent[top] >= ind) top--
172
- top++; indent[top] = ind; keyat[top] = key
173
- path = ""
174
- for (i = 1; i <= top; i++) path = path (i > 1 ? "." : "") keyat[i]
175
- sub(/^[ \t]+/, "", raw)
176
- if (substr(raw, 1, 1) == "{") { flow(path, raw); next }
177
- emit(path, clean(raw))
178
- }
179
- ' "$CONFIG"
180
- }
181
-
182
- off() { echo "kanban: $1"; exit 0; }
183
-
184
- cfg=""
185
- if [ "$board" = auto ] || [ -n "$(default_heading "$col")" ]; then
186
- [ -n "$project" ] || project=$(read_project_name "$profile")
187
- [ -f "$CONFIG" ] && cfg=$(read_config "$project") || cfg=""
188
- fi
189
-
190
- # ── resolve the board ───────────────────────────────────────────────────────
191
- if [ "$board" = auto ]; then
192
- [ -f "$CONFIG" ] || off "no config at $CONFIG — board mirror off"
193
- vault=""; rel=""; enabled=""; kenabled=""
194
- OLDIFS=$IFS; IFS=$TAB
195
- while read -r k v; do
196
- case "$k" in
197
- ENABLED) enabled=$v ;;
198
- KENABLED) kenabled=$v ;;
199
- VAULT) vault=$v ;;
200
- BOARD) rel=$v ;;
201
- esac
202
- done <<EOF
203
- $cfg
204
- EOF
205
- IFS=$OLDIFS
206
- [ "$enabled" != false ] || off "cohorte capabilities disabled (enabled: false in $CONFIG)"
207
- [ "$kenabled" != false ] || off "disabled in $CONFIG (kanban.enabled: false)"
208
- [ -n "$project" ] || off "no project name in $profile — cannot resolve a board"
209
- [ -n "$rel" ] || off "no board configured for project \"$project\" in $CONFIG"
210
- [ -n "$vault" ] || off "obsidian.vault_path not set in $CONFIG"
211
- board="$vault/$rel"
212
- [ -f "$board" ] || off "board file not found: $board"
213
- if $check_only; then echo "kanban: project \"$project\" -> $board"; exit 0; fi
214
- fi
215
-
216
- # ── resolve the column ──────────────────────────────────────────────────────
217
- # A stage key becomes a heading; a literal heading passes through untouched.
218
- if [ -n "$(default_heading "$col")" ]; then
219
- heading=""
220
- OLDIFS=$IFS; IFS=$TAB
221
- while read -r k v; do
222
- case "$k" in
223
- "BCOL.$col") heading=$v ;;
224
- "COL.$col") [ -n "$heading" ] || heading=$v ;;
225
- esac
226
- done <<EOF
227
- $cfg
228
- EOF
229
- IFS=$OLDIFS
230
- [ -n "$heading" ] || heading=$(default_heading "$col")
231
- col="$heading"
232
- fi
233
-
234
- [ -f "$board" ] || { echo "error: board not found: $board" >&2; exit 3; }
235
-
236
- tmp="${board}.kanban-move.$$"
237
- ID="$id" COL="$col" PR="$pr" TITLE="${title:-$id}" awk '
238
- # Emit one line, collapsing runs of blank lines to a single one. Without this
239
- # every move netted +1 blank line in the target column (the card is inserted
240
- # with its own separator, right after the blank that already preceded the next
241
- # heading) — ten moves of one card padded a board with fifteen blank lines.
242
- function emit(s) {
243
- if (s == "") { if (lastblank) return; lastblank = 1 } else lastblank = 0
244
- print s
245
- }
246
- function put_card( k) {
247
- emit(card)
248
- for (k = 1; k <= sn; k++) emit(snote[k])
249
- }
250
- BEGIN {
251
- id = ENVIRON["ID"]; col = tolower(ENVIRON["COL"])
252
- pr = ENVIRON["PR"]; title = ENVIRON["TITLE"]
253
- n = 0; card = ""; found = 0; colseen = 0; sn = 0; lastblank = 0
254
- }
255
- # Strip CR at capture: a CRLF board (Windows-synced vault) otherwise fails the
256
- # `h == col` heading compare and exits 3 on a column that exists — while the
257
- # config parser in this same script already strips \r. Emitting LF-only is fine
258
- # (Obsidian reads both).
259
- { sub(/\r$/, ""); lines[++n] = $0 }
260
- END {
261
- # pass 1: extract the FIRST card block tagged #id; mark every block for removal
262
- for (i = 1; i <= n; i++) {
263
- l = lines[i]
264
- if (l !~ /^- \[.\] /) continue
265
- p = index(l, "#" id)
266
- if (p == 0) continue
267
- # word-boundary check: char after the tag must not extend the id
268
- rest = substr(l, p + length(id) + 1, 1)
269
- if (rest != "" && rest ~ /[A-Za-z0-9_-]/) continue
270
- # Capture the sub-notes of the FIRST match only. Testing `card == lines[i]`
271
- # instead also captured a later duplicate whose text happened to match.
272
- isfirst = 0
273
- if (!found) { found = 1; card = l; isfirst = 1 }
274
- del[i] = 1
275
- for (j = i + 1; j <= n && lines[j] ~ /^[ \t]/; j++) {
276
- del[j] = 1
277
- if (isfirst) snote[++sn] = lines[j]
278
- }
279
- i = j - 1
280
- }
281
- if (!found) card = "- [ ] " title " #" id
282
- if (pr != "" && index(card, "PR #") == 0) card = card " — PR #" pr
283
-
284
- # pass 2: emit, skipping removed blocks; append the card at the end of the
285
- # target column (before the next ## heading / settings block / EOF)
286
- intarget = 0; placed = 0
287
- for (i = 1; i <= n; i++) {
288
- if (del[i]) continue
289
- l = lines[i]
290
- atheading = (l ~ /^## /)
291
- atsettings = (l ~ /^%% kanban:settings/)
292
- if (intarget && (atheading || atsettings)) {
293
- # can not back up over already-printed lines; place the card just before
294
- # this boundary instead
295
- put_card()
296
- emit("")
297
- intarget = 0; placed = 1
298
- }
299
- if (atheading) {
300
- h = tolower(l); sub(/^## +/, "", h); gsub(/ +$/, "", h)
301
- if (h == col) { intarget = 1; colseen = 1 }
302
- }
303
- emit(l)
304
- }
305
- if (intarget && !placed) {
306
- put_card()
307
- placed = 1
308
- }
309
- if (!colseen) exit 3
310
- }
311
- ' "$board" > "$tmp" || { rc=$?; rm -f "$tmp"; [ "$rc" = 3 ] && echo "error: column \"$col\" not found in $board" >&2; exit "$rc"; }
312
- mv "$tmp" "$board"
313
- echo "moved #$id -> $col${pr:+ (PR #$pr)}"