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.
- package/README.md +7 -0
- package/assets/manifest.json +193 -0
- package/assets/migrations/config/README.md +12 -0
- package/assets/migrations/state/0001_init.sql +154 -0
- package/assets/prompts/README.md +11 -0
- package/assets/prompts/agents/fixer.md +7 -0
- package/assets/prompts/agents/implementer.md +12 -0
- package/assets/prompts/agents/reviewer.md +8 -0
- package/assets/prompts/agents/security-reviewer.md +7 -0
- package/assets/prompts/phases/build.md +5 -0
- package/assets/prompts/phases/review.md +5 -0
- package/assets/prompts/phases/test.md +5 -0
- package/assets/prompts/system.md +11 -0
- package/assets/schemas/agent-output.schema.json +225 -0
- package/assets/schemas/auth-status.schema.json +87 -0
- package/assets/schemas/command-result.schema.json +192 -0
- package/assets/schemas/commands.schema.json +952 -0
- package/assets/schemas/config.schema.json +2259 -0
- package/assets/schemas/doctor-report.schema.json +104 -0
- package/assets/schemas/events.schema.json +8514 -0
- package/assets/schemas/fake-script.schema.json +549 -0
- package/assets/schemas/inspect.schema.json +3133 -0
- package/assets/schemas/manifest.schema.json +79 -0
- package/assets/schemas/ownership.schema.json +52 -0
- package/assets/schemas/policy-verdict.schema.json +282 -0
- package/assets/schemas/project-model.schema.json +783 -0
- package/assets/schemas/project-status.schema.json +217 -0
- package/assets/schemas/reconcile-plan.schema.json +155 -0
- package/assets/schemas/run-diff.schema.json +177 -0
- package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
- package/assets/schemas/run-state.schema.json +1755 -0
- package/assets/schemas/runtime-capabilities.schema.json +1545 -0
- package/assets/schemas/sandbox-capabilities.schema.json +116 -0
- package/assets/schemas/skill.schema.json +87 -0
- package/assets/schemas/spec.schema.json +202 -0
- package/assets/schemas/tool-catalogue.schema.json +655 -0
- package/assets/schemas/trust-record.schema.json +38 -0
- package/assets/skills/README.md +9 -0
- package/assets/skills/inspect-surface.md +4 -0
- package/dist/agent-host.mjs +50 -0
- package/dist/agent-host.mjs.map +1 -0
- package/dist/bundle-manifest.json +102 -0
- package/dist/chunks/__host-DgGPyco9.mjs +18 -0
- package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
- package/dist/chunks/approve-Cv135UqI.mjs +19 -0
- package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
- package/dist/chunks/auth-BT5oVp76.mjs +132 -0
- package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
- package/dist/chunks/auth-D8rCVY33.mjs +84 -0
- package/dist/chunks/auth-D8rCVY33.mjs.map +1 -0
- package/dist/chunks/brainstorm-CU10T1tm.mjs +7 -0
- package/dist/chunks/brainstorm-CU10T1tm.mjs.map +1 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
- package/dist/chunks/compose-Cr0hP0qm.mjs +7303 -0
- package/dist/chunks/compose-Cr0hP0qm.mjs.map +1 -0
- package/dist/chunks/config-DefHA2Lw.mjs +96 -0
- package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
- package/dist/chunks/contract-B0ra2-bm.mjs +295 -0
- package/dist/chunks/contract-B0ra2-bm.mjs.map +1 -0
- package/dist/chunks/contract-vYbwYr0Y.mjs +123 -0
- package/dist/chunks/contract-vYbwYr0Y.mjs.map +1 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
- package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
- package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
- package/dist/chunks/discover-30Ciya_0.mjs +20 -0
- package/dist/chunks/discover-30Ciya_0.mjs.map +1 -0
- package/dist/chunks/doctor-DfCrKpfT.mjs +222 -0
- package/dist/chunks/doctor-DfCrKpfT.mjs.map +1 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
- package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
- package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
- package/dist/chunks/init-Okz4mlhW.mjs +30 -0
- package/dist/chunks/init-Okz4mlhW.mjs.map +1 -0
- package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
- package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
- package/dist/chunks/logs-Giq5sUau.mjs +25 -0
- package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
- package/dist/chunks/migrate-DRoFUg53.mjs +19 -0
- package/dist/chunks/migrate-DRoFUg53.mjs.map +1 -0
- package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
- package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
- package/dist/chunks/pause-BASBfKLO.mjs +16 -0
- package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
- package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
- package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
- package/dist/chunks/providers-Dx78imKj.mjs +19 -0
- package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
- package/dist/chunks/reconcile-BfD785gd.mjs +26 -0
- package/dist/chunks/reconcile-BfD785gd.mjs.map +1 -0
- package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
- package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
- package/dist/chunks/retry-CLrUem1H.mjs +15 -0
- package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
- package/dist/chunks/review-BDdjb6kB.mjs +17 -0
- package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
- package/dist/chunks/run-CgTWYlcd.mjs +82 -0
- package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
- package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
- package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
- package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
- package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
- package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
- package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
- package/dist/chunks/src-CId6kMsY.mjs +1689 -0
- package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
- package/dist/chunks/src-CfAOde7C.mjs +5403 -0
- package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
- package/dist/chunks/src-Cu3TxNob.mjs +783 -0
- package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
- package/dist/chunks/src-DcU4IXMe.mjs +117 -0
- package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
- package/dist/chunks/src-JxawBQBn.mjs +446 -0
- package/dist/chunks/src-JxawBQBn.mjs.map +1 -0
- package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
- package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
- package/dist/chunks/status-SMjiOMYy.mjs +29 -0
- package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
- package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
- package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
- package/dist/chunks/update-BXIppsbi.mjs +16 -0
- package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
- package/dist/cli.mjs +175 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +17 -32
- package/CHANGELOG.md +0 -1453
- package/bin/cli.js +0 -812
- package/bin/report.js +0 -141
- package/core/adapter/render.js +0 -389
- package/core/agents/implementer.template.md +0 -142
- package/core/agents/profile-reader.md +0 -50
- package/core/agents/release.md +0 -58
- package/core/agents/review.md +0 -179
- package/core/commands/cohorte-align-ds.md +0 -33
- package/core/commands/cohorte-audit.md +0 -58
- package/core/commands/cohorte-brainstorm.md +0 -70
- package/core/commands/cohorte-build.md +0 -219
- package/core/commands/cohorte-doctor.md +0 -170
- package/core/commands/cohorte-fix.md +0 -80
- package/core/commands/cohorte-fleet.md +0 -103
- package/core/commands/cohorte-init-pipeline.md +0 -31
- package/core/commands/cohorte-intake.md +0 -92
- package/core/commands/cohorte-patch.md +0 -118
- package/core/commands/cohorte-refactor.md +0 -59
- package/core/commands/cohorte-retro.md +0 -85
- package/core/commands/cohorte-review.md +0 -270
- package/core/commands/cohorte-ship.md +0 -122
- package/core/commands/cohorte-spec.md +0 -101
- package/core/commands/cohorte-update-pipeline.md +0 -134
- package/core/hooks/gate.py +0 -580
- package/core/runtimes/claude.json +0 -74
- package/core/runtimes/codex.json +0 -83
- package/core/runtimes/cursor.json +0 -76
- package/core/runtimes/gemini.json +0 -76
- package/core/runtimes/opencode.json +0 -73
- package/core/templates/agent-handoff.md +0 -34
- package/core/templates/brainstorm-return.md +0 -36
- package/core/templates/decisions.template.md +0 -42
- package/core/templates/design-brief.md +0 -44
- package/core/templates/patch.template.md +0 -86
- package/core/templates/pr-body.md +0 -25
- package/core/templates/review-feedback.md +0 -38
- package/core/templates/spec.template.md +0 -87
- package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
- package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
- package/core/templates/steps/init-pipeline/04-write-render.md +0 -115
- package/core/templates/steps/init-pipeline/05-report.md +0 -12
- package/core/workflows/audit.js +0 -240
- package/core/workflows/loop.js +0 -641
- package/core/workflows/refactor.js +0 -276
- package/core/workflows/review.js +0 -456
- package/install.ps1 +0 -103
- package/install.sh +0 -114
- package/lib/doctor.js +0 -504
- package/lib/runtime.js +0 -134
- package/lib/versions.js +0 -149
- package/lib/yaml.js +0 -129
- package/profile/PIPELINE.template.md +0 -251
- package/profile/SCHEMA.md +0 -698
- package/profile/cohorte.config.template.yaml +0 -43
- package/scripts/assert-gate-hook.mjs +0 -75
- package/scripts/kanban-move.sh +0 -313
- package/scripts/metrics/collect.mjs +0 -531
- package/scripts/metrics/prices.json +0 -42
- package/scripts/new-feature.sh.template +0 -98
- package/scripts/preflight.sh +0 -124
- package/scripts/remove-feature.sh.template +0 -56
- package/scripts/test-adapter.mjs +0 -368
- package/scripts/test-gate.mjs +0 -428
- package/scripts/test-kanban.mjs +0 -304
- package/scripts/test-lib.mjs +0 -310
- package/scripts/test-metrics.mjs +0 -156
- package/scripts/test-workflows.mjs +0 -758
- 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)}`);
|
package/scripts/kanban-move.sh
DELETED
|
@@ -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)}"
|