jackherd 1.24.0 → 1.26.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/CHANGELOG.md +9 -0
- package/INSTALL.md +1 -1
- package/README.md +22 -22
- package/dist/{chunk-JDO6O3UO.js → chunk-O365KHHP.js} +139 -3
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -1
- package/dist/plan-format.d.ts +121 -3
- package/dist/plan-format.js +176 -35
- package/docs/plan-contract.md +52 -5
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/hooks/hooks/approval-gate/index.ts +2 -2
- package/kit/hooks/hooks/merge-guard/lane-plan.ts +1 -1
- package/kit/hooks/hooks/open-question/record.ts +81 -1
- package/kit/hooks/hooks/open-question/run.ts +17 -3
- package/kit/hooks/hooks/question-session-end/index.ts +18 -0
- package/kit/hooks/hooks/question-session-end/run.ts +37 -0
- package/kit/hooks/hooks/sent-file/record.ts +4 -1
- package/kit/hooks/hooks/shared/board-lanes.ts +33 -12
- package/kit/hooks/hooks/shared/boards.ts +22 -6
- package/kit/hooks/hooks.json +8 -0
- package/kit/workflow/skills/orchestrate/references/herd.md +3 -3
- package/kit/workflow/skills/orchestrate/references/waiting.md +15 -1
- package/kit/workflow/skills/plan/SKILL.md +3 -1
- package/kit/workflow/skills/plan/references/template-English.md +2 -0
- package/kit/workflow/skills/plan/references/template-German.md +2 -0
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +7 -0
- package/src/brief/command.ts +1 -1
- package/src/brief/lane-facts.ts +2 -2
- package/src/check/command.ts +2 -1
- package/src/check/plan-structure-check.ts +68 -14
- package/src/check/settings.ts +6 -1
- package/src/check/skill-budget-defaults.ts +9 -4
- package/src/config/schema.ts +3 -0
- package/src/lane/close-board.ts +4 -4
- package/src/lane/close-command.ts +2 -2
- package/src/lane/dev-command.ts +1 -1
- package/src/lane/port-command.ts +1 -1
- package/src/plan-contract/command.ts +2 -1
- package/src/plan-contract/contract.ts +30 -9
- package/src/plan-format/bare-codes.ts +4 -4
- package/src/plan-format/dependencies.ts +5 -5
- package/src/plan-format/lanes.ts +3 -4
- package/src/plan-format/markdown.ts +125 -10
- package/src/plan-format/open-status.ts +15 -0
- package/src/plan-format/phases.ts +45 -4
- package/src/plan-format/plan-head.ts +31 -9
- package/src/plan-format/plan-structure.ts +43 -7
- package/src/plan-format/progress.ts +3 -0
- package/src/plan-format/sprint-document.ts +4 -4
- package/src/plan-format/unrecognized-sections.ts +21 -0
- package/src/plan-format/waiting.ts +13 -13
- package/src/plans/board-close.ts +25 -16
- package/src/plans/board-pick.ts +1 -1
- package/src/plans/closable-records.ts +11 -10
- package/src/plans/close-reason.ts +19 -0
- package/src/plans/ensure-panes.ts +2 -2
- package/src/plans/layout-summary.ts +8 -1
- package/src/plans/layout.ts +14 -5
- package/src/plans/live.ts +3 -2
- package/src/plans/question-records.ts +23 -2
- package/src/plans/questions-read.ts +2 -2
- package/src/plans/sent-files-read.ts +2 -2
- package/src/plans/sent-files.ts +12 -4
- package/src/plans/settings.ts +3 -0
- package/src/plans/snapshot.ts +9 -5
- package/src/plans/sprint-export.ts +1 -1
- package/src/plans/texts.ts +3 -0
- package/src/plans/view.ts +1 -0
- package/src/resume/coordinator-read.ts +2 -2
- package/src/settled/command.ts +1 -1
- package/src/settled/settle.ts +5 -5
- package/src/sync/render.ts +20 -5
- package/src/sync/sync-plan.ts +6 -2
- package/src/sync/sync.ts +3 -2
- package/src/vocabulary/index.ts +192 -4
- package/src/waiting/command.ts +82 -26
- package/src/watch/appointments.ts +13 -20
- package/src/watch/command.ts +100 -6
- package/src/watch/lane-progress.ts +1 -1
- package/src/watch/lane-report.ts +4 -0
- package/src/watch/merge-overlap-read.ts +2 -2
- package/src/watch/obsolete-proposals.ts +175 -0
- package/src/watch/obsolete-records.ts +77 -0
- package/src/watch/plan-missing-read.ts +3 -1
- package/src/watch/reference-paths.ts +2 -0
- package/src/watch/watch-events.ts +20 -0
package/dist/plan-format.js
CHANGED
|
@@ -3,25 +3,40 @@ import {
|
|
|
3
3
|
APPOINTMENT_VOCABULARY,
|
|
4
4
|
APPROVAL_TEXTS,
|
|
5
5
|
CARD_FIELDS,
|
|
6
|
+
CELL_KEY,
|
|
7
|
+
COLUMN_KEY,
|
|
8
|
+
HEAD_KEY,
|
|
6
9
|
HEAD_LINE,
|
|
10
|
+
LANE_CLOSED_WORDS,
|
|
7
11
|
LANE_PROMPT_OPENING,
|
|
8
12
|
LANGUAGE_NAME,
|
|
13
|
+
LINE_KEY,
|
|
14
|
+
MARKER_IN_TEXT,
|
|
9
15
|
NEXT_VOCABULARY,
|
|
16
|
+
PARTIAL_PHASE_WORDS,
|
|
10
17
|
PLAN_LANGUAGE,
|
|
11
18
|
PLAN_STATUS,
|
|
12
19
|
PLAN_VOCABULARY,
|
|
13
20
|
RECOMMENDED_MARK,
|
|
14
21
|
REPORT_FIELDS,
|
|
15
22
|
RESPONSE_ADDRESS,
|
|
23
|
+
SECTION_KEY,
|
|
16
24
|
SELF_MERGE_LABEL,
|
|
17
25
|
STANDING_APPROVAL_TEXTS,
|
|
26
|
+
STATUS_KEY,
|
|
18
27
|
WAITING_VOCABULARY,
|
|
19
28
|
classifyStatus,
|
|
29
|
+
columnWords,
|
|
30
|
+
headingOfSection,
|
|
20
31
|
isPlanLanguage,
|
|
32
|
+
isSectionKey,
|
|
33
|
+
isStatusKey,
|
|
34
|
+
languagesFrom,
|
|
21
35
|
phaseHeading,
|
|
22
36
|
sectionHeading,
|
|
37
|
+
sectionOfHeading,
|
|
23
38
|
terminalSafe
|
|
24
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-O365KHHP.js";
|
|
25
40
|
|
|
26
41
|
// src/plan-format/lanes.ts
|
|
27
42
|
import { posix } from "path";
|
|
@@ -30,18 +45,74 @@ import { posix } from "path";
|
|
|
30
45
|
var SECTION_END = /^##\s/;
|
|
31
46
|
var TABLE_SEPARATOR = /^\|[\s|:-]+\|$/;
|
|
32
47
|
var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
-
var
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
var MARKER = /^<!--\s*jh:\s*([a-z-]+)\s*-->\s*$/;
|
|
49
|
+
var markedKeyOf = (text) => MARKER_IN_TEXT.exec(text)?.[1] ?? null;
|
|
50
|
+
var withoutMarker = (text) => text.replace(MARKER_IN_TEXT, "").trim();
|
|
51
|
+
var lookedForSection = (key, level, language) => {
|
|
52
|
+
const headings = languagesFrom(language).map((candidate) => `"${level} ${headingOfSection(key, candidate)}"`);
|
|
53
|
+
return `(looked for ${headings.join(", ")} or <!-- jh: ${key} -->)`;
|
|
54
|
+
};
|
|
55
|
+
var namesState = (text, key, words) => markedKeyOf(text) === key || words.some((word) => text.includes(word));
|
|
56
|
+
var SECTION_HEADING = /^##\s+(.*?)\s*$/;
|
|
57
|
+
var markerKey = (line) => {
|
|
58
|
+
const key = line === void 0 ? void 0 : MARKER.exec(line)?.[1];
|
|
59
|
+
return key !== void 0 && isSectionKey(key) ? key : null;
|
|
60
|
+
};
|
|
61
|
+
var SECTION_MATCH = { pattern: SECTION_HEADING, matches: (text, heading) => text === heading };
|
|
62
|
+
var SUBSECTION_MATCH = {
|
|
63
|
+
pattern: /^(#{2,4})\s+(.*?)\s*$/,
|
|
64
|
+
matches: (text, heading) => text.startsWith(heading)
|
|
65
|
+
};
|
|
66
|
+
var headingText = (line, { pattern }) => pattern.exec(line)?.at(-1);
|
|
67
|
+
var headingIndexOf = (lines, key, language, match) => {
|
|
68
|
+
const marked = lines.findIndex((line, index) => match.pattern.test(line) && markerKey(lines[index + 1]) === key);
|
|
69
|
+
if (marked !== -1) return marked;
|
|
70
|
+
for (const candidate of languagesFrom(language)) {
|
|
71
|
+
const heading = headingOfSection(key, candidate);
|
|
72
|
+
const found = lines.findIndex((line) => {
|
|
73
|
+
const text = headingText(line, match);
|
|
74
|
+
return text !== void 0 && match.matches(text, heading);
|
|
75
|
+
});
|
|
76
|
+
if (found !== -1) return found;
|
|
77
|
+
}
|
|
78
|
+
return -1;
|
|
37
79
|
};
|
|
38
|
-
var
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
80
|
+
var findSectionStart = (markdown, key, language) => {
|
|
81
|
+
const lines = markdown.split("\n");
|
|
82
|
+
const heading = headingIndexOf(lines, key, language, SECTION_MATCH);
|
|
83
|
+
if (heading === -1) return -1;
|
|
84
|
+
return heading + (markerKey(lines[heading + 1]) === key ? 2 : 1);
|
|
85
|
+
};
|
|
86
|
+
var findSectionHeading = (markdown, key, language) => headingIndexOf(markdown.split("\n"), key, language, SECTION_MATCH);
|
|
87
|
+
var untilNextSection = (rest) => {
|
|
42
88
|
const end = rest.findIndex((line) => SECTION_END.test(line));
|
|
43
89
|
return end === -1 ? rest : rest.slice(0, end);
|
|
44
90
|
};
|
|
91
|
+
var columnIndex = (header, key, language) => {
|
|
92
|
+
const marked = header.findIndex((cell) => markedKeyOf(cell) === key);
|
|
93
|
+
if (marked !== -1) return marked;
|
|
94
|
+
const names = header.map(withoutMarker);
|
|
95
|
+
for (const candidate of languagesFrom(language)) {
|
|
96
|
+
for (const word of columnWords(key, candidate)) {
|
|
97
|
+
const found = names.indexOf(word);
|
|
98
|
+
if (found !== -1) return found;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return -1;
|
|
102
|
+
};
|
|
103
|
+
var exactSectionStart = (markdown, headingTextValue) => {
|
|
104
|
+
const heading = new RegExp(`^##\\s+${escapeRegExp(headingTextValue)}\\s*$`);
|
|
105
|
+
const start = markdown.split("\n").findIndex((line) => heading.test(line));
|
|
106
|
+
return start === -1 ? -1 : start + 1;
|
|
107
|
+
};
|
|
108
|
+
var sectionStart = (markdown, headingText2) => {
|
|
109
|
+
const section = sectionOfHeading(headingText2);
|
|
110
|
+
return section === null ? exactSectionStart(markdown, headingText2) : findSectionStart(markdown, section.key, section.language);
|
|
111
|
+
};
|
|
112
|
+
var sectionLines = (markdown, headingText2) => {
|
|
113
|
+
const start = sectionStart(markdown, headingText2);
|
|
114
|
+
return start === -1 ? [] : untilNextSection(markdown.split("\n").slice(start));
|
|
115
|
+
};
|
|
45
116
|
var tableCells = (line) => line.slice(1, line.endsWith("|") ? -1 : void 0).split("|").map((cell) => terminalSafe(cell).trim());
|
|
46
117
|
var firstTableRows = (lines) => {
|
|
47
118
|
const start = lines.findIndex((line) => line.startsWith("|"));
|
|
@@ -57,10 +128,14 @@ var headerField = (markdown, name) => {
|
|
|
57
128
|
};
|
|
58
129
|
var firstHeading = (markdown, fallback) => terminalSafe(markdown.match(/^#\s+(.+)$/m)?.[1] ?? fallback).trim();
|
|
59
130
|
var HEADING = /^(#{2,4})\s+(.*?)\s*$/;
|
|
60
|
-
var subsectionStart = (markdown,
|
|
61
|
-
|
|
131
|
+
var subsectionStart = (markdown, headingText2) => {
|
|
132
|
+
const section = sectionOfHeading(headingText2);
|
|
133
|
+
if (section !== null) return headingIndexOf(markdown.split("\n"), section.key, section.language, SUBSECTION_MATCH);
|
|
134
|
+
return markdown.split("\n").findIndex((line) => HEADING.exec(line)?.[2].startsWith(headingText2));
|
|
135
|
+
};
|
|
136
|
+
var subsectionLines = (markdown, headingText2) => {
|
|
62
137
|
const lines = markdown.split("\n");
|
|
63
|
-
const start = subsectionStart(markdown,
|
|
138
|
+
const start = subsectionStart(markdown, headingText2);
|
|
64
139
|
if (start === -1) return [];
|
|
65
140
|
const level = HEADING.exec(lines[start])?.[1].length ?? 2;
|
|
66
141
|
const rest = lines.slice(start + 1);
|
|
@@ -166,14 +241,13 @@ var withoutRepeats = (lane, step) => {
|
|
|
166
241
|
var parseSprintLanes = (markdown, language) => {
|
|
167
242
|
const lanes = /* @__PURE__ */ new Map();
|
|
168
243
|
const phaseId = phaseIdPatterns(language);
|
|
169
|
-
const { laneClosed } = PLAN_VOCABULARY[language];
|
|
170
244
|
let previous = null;
|
|
171
245
|
for (const cells of firstTable(sectionLines(markdown, PLAN_VOCABULARY[language].sections.lanes))) {
|
|
172
246
|
const name = laneNameOf(cells[0] ?? "", previous);
|
|
173
247
|
if (!name) continue;
|
|
174
248
|
const port = Number.parseInt(cells[1] ?? "", 10);
|
|
175
249
|
const lane = lanes.get(name) ?? { name, port: Number.isNaN(port) ? null : port, steps: [], closed: false };
|
|
176
|
-
const marked = (cells[0] ?? ""
|
|
250
|
+
const marked = namesState(cells[0] ?? "", CELL_KEY.laneClosed, LANE_CLOSED_WORDS);
|
|
177
251
|
if (lane.closed && !marked && !(cells[0] ?? "").startsWith(CONTINUATION)) {
|
|
178
252
|
lane.closed = false;
|
|
179
253
|
lane.steps = [];
|
|
@@ -227,7 +301,7 @@ var phaseWorkFields = (work) => work === PHASE_WORK.merged ? { work, touched: tr
|
|
|
227
301
|
var REVIEW_GATE = /^\s*\*\*review[- ]gate\b/i;
|
|
228
302
|
var PHASE_END = /^#{1,3}\s/;
|
|
229
303
|
var CHECKBOX = /^\s*[-*]\s+\[([ xX])\]\s+(.*)$/;
|
|
230
|
-
var
|
|
304
|
+
var SECTION_HEADING2 = /^##\s+(.+?)\s*$/;
|
|
231
305
|
var TOP_HEADING = /^#\s/;
|
|
232
306
|
var TABLE_STATUS = { done: "\u2705", open: "\u2B1C" };
|
|
233
307
|
var ROW_NUMBER = /^\d+(\.\d+)*[a-z]?$/;
|
|
@@ -240,6 +314,19 @@ var endsPhase = (line) => PHASE_END.test(line);
|
|
|
240
314
|
var isCheckboxLine = (line) => CHECKBOX.test(line);
|
|
241
315
|
var isOpenCheckboxLine = (line) => line.match(CHECKBOX)?.[1] === " ";
|
|
242
316
|
var phaseHeadingPattern = (language) => new RegExp(`^###\\s+${escapeRegExp(PLAN_VOCABULARY[language].phase)}\\s+(\\d+[a-z]?)\\b\\s*[:\u2014\u2013-]?\\s*(.*)$`, "i");
|
|
317
|
+
var BOLD_OPENING = /^\s*\*\*([^*]+?)\s*:?\*\*\s*:?\s*(.*)$/;
|
|
318
|
+
var namesPurpose = (line, label, language) => {
|
|
319
|
+
if (markedKeyOf(line) === LINE_KEY.purpose) return true;
|
|
320
|
+
const written = label.trim().toLowerCase();
|
|
321
|
+
return languagesFrom(language).some((candidate) => PLAN_VOCABULARY[candidate].phasePurpose.toLowerCase() === written);
|
|
322
|
+
};
|
|
323
|
+
var phasePurposeOf = (line, language) => {
|
|
324
|
+
const opening = BOLD_OPENING.exec(line);
|
|
325
|
+
if (!opening || !namesPurpose(line, opening[1], language)) return null;
|
|
326
|
+
const text = terminalSafe(withoutMarker(opening[2])).trim();
|
|
327
|
+
return text === "" ? null : text;
|
|
328
|
+
};
|
|
329
|
+
var decidesPurpose = (decided, line) => !decided && line.trim() !== "";
|
|
243
330
|
var tableCells2 = (line) => {
|
|
244
331
|
const trimmed = line.trim();
|
|
245
332
|
if (!trimmed.startsWith("|")) return null;
|
|
@@ -266,7 +353,7 @@ var sectionItems = (markdown) => {
|
|
|
266
353
|
const sections = /* @__PURE__ */ new Map();
|
|
267
354
|
let open = null;
|
|
268
355
|
for (const line of markdown.split("\n")) {
|
|
269
|
-
const heading = line.match(
|
|
356
|
+
const heading = line.match(SECTION_HEADING2);
|
|
270
357
|
if (heading?.[1]) {
|
|
271
358
|
open = [];
|
|
272
359
|
sections.set(heading[1], open);
|
|
@@ -341,6 +428,12 @@ var notePointer = (pointers, phase, line) => {
|
|
|
341
428
|
const pointer = phase && !pointers.has(phase) ? line.match(SECTION_POINTER)?.[1] : void 0;
|
|
342
429
|
if (phase && pointer) pointers.set(phase, pointer);
|
|
343
430
|
};
|
|
431
|
+
var notePurpose = (read, phase, line, language) => {
|
|
432
|
+
if (!phase || !decidesPurpose(read.has(phase), line)) return;
|
|
433
|
+
read.add(phase);
|
|
434
|
+
const purpose = phasePurposeOf(line, language);
|
|
435
|
+
if (purpose !== null) phase.purpose = purpose;
|
|
436
|
+
};
|
|
344
437
|
var headingOf = (line, phaseHeading2) => line.match(phaseHeading2) ?? line.match(NUMBERED_SECTION);
|
|
345
438
|
var phaseHeadings = (markdown, language) => {
|
|
346
439
|
const phaseHeading2 = phaseHeadingPattern(language);
|
|
@@ -352,15 +445,22 @@ var phaseHeadings = (markdown, language) => {
|
|
|
352
445
|
var parsePlanPhases = (markdown, language) => {
|
|
353
446
|
const phaseHeading2 = phaseHeadingPattern(language);
|
|
354
447
|
const pointers = /* @__PURE__ */ new Map();
|
|
448
|
+
const purposeRead = /* @__PURE__ */ new Set();
|
|
355
449
|
const { phases } = markdown.split("\n").reduce(
|
|
356
450
|
({ phases: phases2, open, gate }, line) => {
|
|
357
451
|
const heading = headingOf(line, phaseHeading2);
|
|
358
452
|
if (heading) {
|
|
359
|
-
const phase = {
|
|
453
|
+
const phase = {
|
|
454
|
+
id: (heading[1] ?? "").toLowerCase(),
|
|
455
|
+
title: terminalSafe(heading[2] ?? "").trim(),
|
|
456
|
+
items: []
|
|
457
|
+
};
|
|
458
|
+
if (!phaseHeading2.test(line)) purposeRead.add(phase);
|
|
360
459
|
return { phases: [...phases2, phase], open: phase, gate: false };
|
|
361
460
|
}
|
|
362
461
|
if (PHASE_END.test(line)) return { phases: phases2, open: null, gate: false };
|
|
363
462
|
const inGate = gate || REVIEW_GATE.test(line);
|
|
463
|
+
notePurpose(purposeRead, open, line, language);
|
|
364
464
|
if (open) fileLine(open, line, inGate);
|
|
365
465
|
notePointer(pointers, open, line);
|
|
366
466
|
return { phases: phases2, open, gate: inGate };
|
|
@@ -385,6 +485,7 @@ var phaseProgress = (phases, id) => {
|
|
|
385
485
|
done: items.filter((item) => item.done).length,
|
|
386
486
|
total: items.length,
|
|
387
487
|
items,
|
|
488
|
+
...phase?.purpose ? { purpose: phase.purpose } : {},
|
|
388
489
|
...phase?.deferred ? { deferred: phase.deferred } : {},
|
|
389
490
|
...phase?.work ? phaseWorkFields(phase.work) : {}
|
|
390
491
|
};
|
|
@@ -546,10 +647,10 @@ var releasedByFiles = (writes) => writes.flatMap((path) => {
|
|
|
546
647
|
return [packageKey(directory.at(-1) ?? ".")];
|
|
547
648
|
});
|
|
548
649
|
var parseDependencyTable = (markdown, language) => {
|
|
549
|
-
const { sections
|
|
650
|
+
const { sections } = PLAN_VOCABULARY[language];
|
|
550
651
|
const { header, rows } = firstTableWithHeader(subsectionLines(markdown, sections.dependencies));
|
|
551
|
-
const [after, writes, marker] = [
|
|
552
|
-
(
|
|
652
|
+
const [after, writes, marker] = [COLUMN_KEY.dependsOn, COLUMN_KEY.writes, COLUMN_KEY.serialMarker].map(
|
|
653
|
+
(key) => columnIndex(header, key, language)
|
|
553
654
|
);
|
|
554
655
|
if (after === -1 || writes === -1 || marker === -1) return null;
|
|
555
656
|
return rows.flatMap((cells) => {
|
|
@@ -790,16 +891,21 @@ var tallyPhases = (markdown, language) => {
|
|
|
790
891
|
|
|
791
892
|
// src/plan-format/plan-head.ts
|
|
792
893
|
var HEAD_STATUS = { present: "present", missing: "missing", stale: "stale" };
|
|
793
|
-
var
|
|
894
|
+
var SECTION_HEADING3 = /^##\s/;
|
|
794
895
|
var HEAD_ENTRY = /^\s*(?:[-*]\s+)?\*\*([^*]+?)\s*:?\*\*\s*:?\s*(.*)$/;
|
|
795
896
|
var HEAD_LINES = Object.values(HEAD_LINE);
|
|
796
897
|
var labelOf = (language, line) => PLAN_VOCABULARY[language].head[line].toLowerCase();
|
|
898
|
+
var labelNamed = (text, written, language) => {
|
|
899
|
+
const marked = markedKeyOf(text);
|
|
900
|
+
const byMarker = HEAD_LINES.find((each) => HEAD_KEY[each] === marked);
|
|
901
|
+
if (byMarker) return byMarker;
|
|
902
|
+
return languagesFrom(language).map((candidate) => HEAD_LINES.find((each) => labelOf(candidate, each) === written)).find((each) => each !== void 0);
|
|
903
|
+
};
|
|
797
904
|
var entryOf = (text, language) => {
|
|
798
905
|
const match = text.match(HEAD_ENTRY);
|
|
799
906
|
if (!match) return null;
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
return label ? { label, text: match[2].trim() } : null;
|
|
907
|
+
const label = labelNamed(text, match[1].trim().toLowerCase(), language);
|
|
908
|
+
return label ? { label, text: withoutMarker(match[2]) } : null;
|
|
803
909
|
};
|
|
804
910
|
var phaseOf = (statusText, language) => {
|
|
805
911
|
const match = statusText?.match(
|
|
@@ -809,11 +915,10 @@ var phaseOf = (statusText, language) => {
|
|
|
809
915
|
};
|
|
810
916
|
var parsePlanHead = (markdown, language) => {
|
|
811
917
|
const lines = markdown.split("\n");
|
|
812
|
-
const
|
|
813
|
-
const start = lines.findIndex((text) => heading.test(text));
|
|
918
|
+
const start = findSectionHeading(markdown, SECTION_KEY.forYou, language);
|
|
814
919
|
if (start === -1) return null;
|
|
815
920
|
const rest = lines.slice(start + 1);
|
|
816
|
-
const end = rest.findIndex((text) =>
|
|
921
|
+
const end = rest.findIndex((text) => SECTION_HEADING3.test(text));
|
|
817
922
|
const texts = {};
|
|
818
923
|
const lineNumbers = {};
|
|
819
924
|
for (const [offset, text] of (end === -1 ? rest : rest.slice(0, end)).entries()) {
|
|
@@ -829,7 +934,7 @@ var parsePlanHead = (markdown, language) => {
|
|
|
829
934
|
lineNumbers,
|
|
830
935
|
missing: HEAD_LINES.filter((each) => texts[each] === void 0),
|
|
831
936
|
phase: phaseOf(texts[HEAD_LINE.status], language),
|
|
832
|
-
misplaced: lines.slice(0, start).some((text) =>
|
|
937
|
+
misplaced: lines.slice(0, start).some((text) => SECTION_HEADING3.test(text))
|
|
833
938
|
};
|
|
834
939
|
};
|
|
835
940
|
var verdict = (status, line, message) => ({ status, line, message });
|
|
@@ -843,8 +948,14 @@ var phaseMismatch = ({ current, total }, states) => {
|
|
|
843
948
|
};
|
|
844
949
|
var checkPlanHead = (markdown, language) => {
|
|
845
950
|
const head = parsePlanHead(markdown, language);
|
|
846
|
-
|
|
847
|
-
|
|
951
|
+
if (head === null) {
|
|
952
|
+
const looked = lookedForSection(SECTION_KEY.forYou, "##", language);
|
|
953
|
+
return verdict(
|
|
954
|
+
HEAD_STATUS.missing,
|
|
955
|
+
null,
|
|
956
|
+
`no \`## ${PLAN_VOCABULARY[language].sections.forYou}\` section ${looked}`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
848
959
|
if (head.misplaced)
|
|
849
960
|
return verdict(
|
|
850
961
|
HEAD_STATUS.stale,
|
|
@@ -864,15 +975,21 @@ var checkPlanHead = (markdown, language) => {
|
|
|
864
975
|
};
|
|
865
976
|
|
|
866
977
|
// src/plan-format/plan-structure.ts
|
|
867
|
-
var
|
|
978
|
+
var NO_GATE_WORDS = new RegExp(
|
|
979
|
+
`^\\s*\\*\\*(?:${Object.values(PLAN_VOCABULARY).map(({ noReviewGate }) => escapeRegExp(noReviewGate)).join("|")})\\b`,
|
|
980
|
+
"i"
|
|
981
|
+
);
|
|
982
|
+
var isNoGateLine = (line) => NO_GATE_WORDS.test(line) || BOLD_OPENING.test(line) && markedKeyOf(line) === LINE_KEY.noReviewGate;
|
|
868
983
|
var tickCount = (phase) => phase.items.filter((item) => item.done).length;
|
|
869
984
|
var tasksDone = (phase) => {
|
|
870
985
|
const tasks = phase.items.filter((item) => !item.gate);
|
|
871
986
|
return tasks.length > 0 && tasks.every((item) => item.done);
|
|
872
987
|
};
|
|
988
|
+
var settlePurpose = (section, line, language) => {
|
|
989
|
+
if (decidesPurpose(section.purpose !== null, line)) section.purpose = phasePurposeOf(line, language) !== null;
|
|
990
|
+
};
|
|
873
991
|
var phaseSections = (markdown, language) => {
|
|
874
992
|
const heading = phaseHeadingPattern(language);
|
|
875
|
-
const noGate = noGatePattern(language);
|
|
876
993
|
const sections = [];
|
|
877
994
|
let open = null;
|
|
878
995
|
for (const [index, line] of markdown.split("\n").entries()) {
|
|
@@ -884,14 +1001,16 @@ var phaseSections = (markdown, language) => {
|
|
|
884
1001
|
title: terminalSafe(match[2]).trim(),
|
|
885
1002
|
tasks: false,
|
|
886
1003
|
gate: false,
|
|
1004
|
+
purpose: null,
|
|
887
1005
|
openLines: []
|
|
888
1006
|
};
|
|
889
1007
|
sections.push(open);
|
|
890
1008
|
} else if (endsPhase(line)) {
|
|
891
1009
|
open = null;
|
|
892
1010
|
} else if (open) {
|
|
1011
|
+
settlePurpose(open, line, language);
|
|
893
1012
|
open.tasks ||= isCheckboxLine(line);
|
|
894
|
-
open.gate ||= isReviewGateLine(line) ||
|
|
1013
|
+
open.gate ||= isReviewGateLine(line) || isNoGateLine(line);
|
|
895
1014
|
if (isOpenCheckboxLine(line) && deferralTarget(line, open.id) === null) open.openLines.push(index + 1);
|
|
896
1015
|
}
|
|
897
1016
|
}
|
|
@@ -899,6 +1018,8 @@ var phaseSections = (markdown, language) => {
|
|
|
899
1018
|
};
|
|
900
1019
|
var phaseIdsOfPlan = (markdown, language) => new Set(phaseSections(markdown, language).map(({ id }) => id));
|
|
901
1020
|
var newPhasesWithoutReviewGate = (markdown, language, previousIds) => phaseSections(markdown, language).filter(({ id, tasks, gate }) => tasks && !gate && !previousIds.has(id)).map(({ line, id, title }) => ({ line, id, title }));
|
|
1021
|
+
var newPhasesWithoutPurpose = (markdown, language, previousIds) => phaseSections(markdown, language).filter(({ id, tasks, purpose }) => tasks && !purpose && !previousIds.has(id)).map(({ line, id, title }) => ({ line, id, title }));
|
|
1022
|
+
var phasesWithoutPurpose = (markdown, language) => newPhasesWithoutPurpose(markdown, language, /* @__PURE__ */ new Set());
|
|
902
1023
|
var hasLanesTable = (markdown, language) => sectionLines(markdown, PLAN_VOCABULARY[language].sections.lanes).some((line) => line.startsWith("|"));
|
|
903
1024
|
var mergedPhasesWithOpenBoxes = (markdown, base, language) => {
|
|
904
1025
|
const baseById = new Map(parsePlanPhases(base, language).map((phase) => [phase.id, phase]));
|
|
@@ -977,9 +1098,9 @@ var readSprintDocument = (markdown, language) => {
|
|
|
977
1098
|
};
|
|
978
1099
|
};
|
|
979
1100
|
var readPhaseDependencies = (planMarkdown, language) => {
|
|
980
|
-
const { sections
|
|
1101
|
+
const { sections } = PLAN_VOCABULARY[language];
|
|
981
1102
|
const { header, rows } = firstTableWithHeader(subsectionLines(planMarkdown, sections.dependencies));
|
|
982
|
-
const column = header.
|
|
1103
|
+
const column = columnIndex(header, COLUMN_KEY.dependsOn, language);
|
|
983
1104
|
if (column === -1) return {};
|
|
984
1105
|
const entries = rows.flatMap((cells) => {
|
|
985
1106
|
const id = PHASE_ID_IN_CELL.exec(cells[0] ?? "")?.[1];
|
|
@@ -991,13 +1112,21 @@ export {
|
|
|
991
1112
|
ADDRESS_FORM,
|
|
992
1113
|
APPOINTMENT_VOCABULARY,
|
|
993
1114
|
APPROVAL_TEXTS,
|
|
1115
|
+
BOLD_OPENING,
|
|
994
1116
|
CARD_FIELDS,
|
|
1117
|
+
CELL_KEY,
|
|
1118
|
+
COLUMN_KEY,
|
|
1119
|
+
HEAD_KEY,
|
|
995
1120
|
HEAD_LINE,
|
|
996
1121
|
HEAD_STATUS,
|
|
1122
|
+
LANE_CLOSED_WORDS,
|
|
997
1123
|
LANE_PROMPT_OPENING,
|
|
998
1124
|
LANGUAGE_NAME,
|
|
1125
|
+
LINE_KEY,
|
|
1126
|
+
MARKER_IN_TEXT,
|
|
999
1127
|
NEXT_VOCABULARY,
|
|
1000
1128
|
NO_DEPENDENCY_TABLE,
|
|
1129
|
+
PARTIAL_PHASE_WORDS,
|
|
1001
1130
|
PHASE_CELL_LIMIT,
|
|
1002
1131
|
PHASE_ID_PREFIX,
|
|
1003
1132
|
PHASE_TITLE_LIMIT,
|
|
@@ -1009,10 +1138,12 @@ export {
|
|
|
1009
1138
|
RECOMMENDED_MARK,
|
|
1010
1139
|
REPORT_FIELDS,
|
|
1011
1140
|
RESPONSE_ADDRESS,
|
|
1141
|
+
SECTION_KEY,
|
|
1012
1142
|
SELF_MERGE_LABEL,
|
|
1013
1143
|
SHORT_NAME_LIMIT,
|
|
1014
1144
|
SHORT_NAME_PROBLEM,
|
|
1015
1145
|
STANDING_APPROVAL_TEXTS,
|
|
1146
|
+
STATUS_KEY,
|
|
1016
1147
|
WAITING_VOCABULARY,
|
|
1017
1148
|
activeItem,
|
|
1018
1149
|
boardRowsWithProse,
|
|
@@ -1020,15 +1151,18 @@ export {
|
|
|
1020
1151
|
checkPlanHead,
|
|
1021
1152
|
classifyStatus,
|
|
1022
1153
|
columnModels,
|
|
1154
|
+
columnWords,
|
|
1023
1155
|
countDecisions,
|
|
1024
1156
|
criticalPath,
|
|
1025
1157
|
currentPhase,
|
|
1026
1158
|
currentPhaseOf,
|
|
1159
|
+
decidesPurpose,
|
|
1027
1160
|
deferralTarget,
|
|
1028
1161
|
differsFromColumn,
|
|
1029
1162
|
endsPhase,
|
|
1030
1163
|
findDrift,
|
|
1031
1164
|
hasLanesTable,
|
|
1165
|
+
headingOfSection,
|
|
1032
1166
|
isCheckboxLine,
|
|
1033
1167
|
isDocumentationGateItem,
|
|
1034
1168
|
isOpenCheckboxLine,
|
|
@@ -1036,12 +1170,16 @@ export {
|
|
|
1036
1170
|
isPhaseOpen,
|
|
1037
1171
|
isPlanLanguage,
|
|
1038
1172
|
isReviewGateLine,
|
|
1173
|
+
isSectionKey,
|
|
1174
|
+
isStatusKey,
|
|
1039
1175
|
isTickedUnmerged,
|
|
1040
1176
|
laneNameOf,
|
|
1041
1177
|
laneProgress,
|
|
1178
|
+
languagesFrom,
|
|
1042
1179
|
longPhaseTitles,
|
|
1043
1180
|
mergedPhasesWithOpenBoxes,
|
|
1044
1181
|
misplacedDeferrals,
|
|
1182
|
+
newPhasesWithoutPurpose,
|
|
1045
1183
|
newPhasesWithoutReviewGate,
|
|
1046
1184
|
onlyDocumentationOpen,
|
|
1047
1185
|
openBoxesOfPhase,
|
|
@@ -1062,15 +1200,18 @@ export {
|
|
|
1062
1200
|
phaseLabel,
|
|
1063
1201
|
phaseLabels,
|
|
1064
1202
|
phaseProgress,
|
|
1203
|
+
phasePurposeOf,
|
|
1065
1204
|
phaseWaves,
|
|
1066
1205
|
phaseWorkFields,
|
|
1067
1206
|
phasesText,
|
|
1068
1207
|
phasesWithoutDependencyRow,
|
|
1208
|
+
phasesWithoutPurpose,
|
|
1069
1209
|
readPhaseDependencies,
|
|
1070
1210
|
readPlanHeader,
|
|
1071
1211
|
readSprintDocument,
|
|
1072
1212
|
resolvePlanPath,
|
|
1073
1213
|
sectionHeading,
|
|
1214
|
+
sectionOfHeading,
|
|
1074
1215
|
shortNameFindings,
|
|
1075
1216
|
shortTitle,
|
|
1076
1217
|
sprintTitleOf,
|
package/docs/plan-contract.md
CHANGED
|
@@ -11,6 +11,7 @@ jackherd reads a plan from its Markdown. This is what a plan has to hold for the
|
|
|
11
11
|
| Model | a table under `### Model recommendations per phase`: the phase in the first cell (`1 — Title`, `P1`, `10c`), the model in the second (`claude-opus-5-5`, `Opus 5.5`); a dash marks a phase that needs no model | yes | lane start (the model a lane runs on), `/implement` |
|
|
12
12
|
| Dependencies | a table under `### Dependencies and parallelization` with the columns `Depends on`, `Writes`, `Serial marker` and one row per phase | from two phases on, optional | `jh check --parallel`, `/implement` |
|
|
13
13
|
| Head | a section `## For you` (de: `## Für dich`) right after the header lines (`**Date:**`, `**Status:**`, `**Scope:**`), before the first other `##` section, with five lines `- **<label>:** <text>` | no, a note | `jh plan check`, the reader who is not an agent |
|
|
14
|
+
| Purpose | directly under every `### Phase` heading, one line `**What it is about:** <sentence>` (de: `**Worum es geht:** <Satz>`): what is different afterwards, for whom | no, a note | `jh plan check`, `jh check` (new phases), the herd |
|
|
14
15
|
|
|
15
16
|
Details that decide a verdict:
|
|
16
17
|
|
|
@@ -21,7 +22,47 @@ Details that decide a verdict:
|
|
|
21
22
|
- The head's five labels are fixed per language. English: `What changes for you`, `Status`, `Your open decisions`, `Try it now`, `Next step`; German: `Was sich für dich ändert`, `Stand`, `Deine offenen Entscheidungen`, `Schon ausprobieren`, `Nächster Schritt`. The colon may stand outside the bold (`**Status**:`) and the list mark may be left out.
|
|
22
23
|
- The `Status` line carries `Phase x/y`: x the id of the first phase with an open checkbox, as its heading spells it (`3`, `3a`), y the number of phases, each id counted once. When every phase is done, x is the id of the last one.
|
|
23
24
|
- The head is `missing` without the section. It is `stale` when it stands behind another section, lacks a line, has no `Phase x/y`, or names another phase or phase count than the checkboxes show. Both are notes: the contract stays met and the exit code does not change, so plans written before the head keep passing.
|
|
24
|
-
- The headings
|
|
25
|
+
- The purpose line counts when it is the first non-empty line under a `### Phase` heading and has text after the colon; the colon may stand outside the bold or be left out. A line behind the origin line does not count. Only `### Phase` headings are read; phases headed `## 1 — …` or `## P1 — …` and the closing `## N — …` section are not. `purpose` is `missing` (`no purpose line in P2, P3`) when a phase with checkboxes of its own lacks the line (a phase that only points at a section needs none); it is a note and the exit code does not change, so older plans keep passing. `jh check` goes further for a phase the change adds: it fails without the line; phases the plan already had are not read.
|
|
26
|
+
- The headings are shown in English here; a plan or board is read in German as well, and in any other language by markers (see below).
|
|
27
|
+
|
|
28
|
+
## Language and markers
|
|
29
|
+
|
|
30
|
+
jackherd reads boards and plans in every language it has words for (English and German), whatever `.jackherd/config.json` says: a German board next to an English plan works, and so does the other way round. The configuration decides only what jh **writes**:
|
|
31
|
+
|
|
32
|
+
| Key | Decides | Default |
|
|
33
|
+
| --- | ------- | ------- |
|
|
34
|
+
| `planLanguage` | the language of new plans and of the plan templates | `en` |
|
|
35
|
+
| `boardLanguage` | the language jh writes the board in: history lines, lane rows, waiting rows | the value of `planLanguage` |
|
|
36
|
+
|
|
37
|
+
A config without `boardLanguage` runs as before. A heading, column name or status word is looked up in this order: the marker, the word of the configured language, the word of the other language.
|
|
38
|
+
|
|
39
|
+
A document in a language jackherd has no words for carries a **marker**, `<!-- jh: <key> -->`, which Markdown does not show. The key comes from a closed set; an unknown key is ignored.
|
|
40
|
+
|
|
41
|
+
| Where the marker stands | Keys |
|
|
42
|
+
| ----------------------- | ---- |
|
|
43
|
+
| on the line directly under a `##` (or `###`) heading | `waiting`, `appointments`, `lanes`, `goal`, `history`, `plans`, `decisions`, `implementation`, `model`, `dependencies`, `documentation`, `for-you`, `next` |
|
|
44
|
+
| in a header cell of a table | `since`, `item`, `origin`, `unblocks`, `status`, `when`, `action`, `depends-on`, `writes`, `serial-marker` |
|
|
45
|
+
| on a line of the head | `head-changes`, `head-status`, `head-decisions`, `head-try-now`, `head-next` |
|
|
46
|
+
| in a status cell | `status-open` (a board row that still waits), `status-done`, `status-abandoned`, `status-in-progress`, `status-draft` |
|
|
47
|
+
| on a line that opens with bold words | `no-review-gate`, `purpose` |
|
|
48
|
+
| in a lane's name cell or phases cell | `lane-closed`, `partial-phase` |
|
|
49
|
+
|
|
50
|
+
```markdown
|
|
51
|
+
## Pistes
|
|
52
|
+
<!-- jh: lanes -->
|
|
53
|
+
|
|
54
|
+
| Piste | Port | Plan |
|
|
55
|
+
| ----- | ---- | ---- |
|
|
56
|
+
| `checkout` nettoyé <!-- jh: lane-closed --> | 3200 | [Plan](checkout.md) |
|
|
57
|
+
|
|
58
|
+
### Phase 1: Surface
|
|
59
|
+
|
|
60
|
+
**Le but** <!-- jh: purpose --> Les clients voient leurs filtres en haut de la liste.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
When no section of a changed plan or board is recognised, three places say so: `jh plan check` names the forms it looked for (`no model table (looked for "### Model recommendations per phase", "### Model-Empfehlungen pro Phase" or <!-- jh: model -->)`), `jh check` warns on a changed file that has `##` headings of which jh reads none, and the herd shows `board: no section recognised` instead of "nothing waits for you". A section that is absent but allowed (no waiting item) stays silent.
|
|
64
|
+
|
|
65
|
+
The plan templates are written in the language of their file name: `template-English.md` always in English, `template-German.md` always in German, whatever `planLanguage` says.
|
|
25
66
|
|
|
26
67
|
## Minimal valid plan
|
|
27
68
|
|
|
@@ -49,11 +90,15 @@ Details that decide a verdict:
|
|
|
49
90
|
|
|
50
91
|
### Phase 1: Query parser
|
|
51
92
|
|
|
93
|
+
**What it is about:** nothing visible yet; the search box can turn typed text into filters.
|
|
94
|
+
|
|
52
95
|
- [ ] **Parse filters**: `parseQuery(text)` returns the filter list
|
|
53
96
|
- [ ] **Test it**: every operator, one malformed input
|
|
54
97
|
|
|
55
98
|
### Phase 2: Filter bar
|
|
56
99
|
|
|
100
|
+
**What it is about:** people who search see their filters as chips above the results and can remove each one.
|
|
101
|
+
|
|
57
102
|
- [ ] **Render the bar**: one chip per filter
|
|
58
103
|
```
|
|
59
104
|
|
|
@@ -70,22 +115,24 @@ phases: present plan.md:22 2 phases
|
|
|
70
115
|
tasks: present plan.md:22 every phase has tasks
|
|
71
116
|
model: present plan.md:12 every phase has a model
|
|
72
117
|
dependencies: present every phase has a row
|
|
73
|
-
head: missing no `## For you` section
|
|
118
|
+
head: missing no `## For you` section (looked for "## For you", "## Für dich" or <!-- jh: for-you -->)
|
|
119
|
+
purpose: present every phase says what it is about
|
|
74
120
|
```
|
|
75
121
|
|
|
76
|
-
If the model table lacked the row for phase 2,
|
|
122
|
+
If the model table lacked the row for phase 2, the dependency table were left out and the phases had no purpose lines, the lines would read:
|
|
77
123
|
|
|
78
124
|
```
|
|
79
125
|
phases: present plan.md:22 2 phases
|
|
80
126
|
tasks: present plan.md:22 every phase has tasks
|
|
81
127
|
model: malformed plan.md:22 no model for P2
|
|
82
|
-
dependencies: missing no dependency table: phases run one after another
|
|
128
|
+
dependencies: missing no dependency table: phases run one after another (looked for "### Dependencies and parallelization", "### Abhängigkeiten und Parallelisierung" or <!-- jh: dependencies -->)
|
|
83
129
|
head: stale plan.md:5 names phase 1, the running phase is 2
|
|
130
|
+
purpose: missing plan.md:22 no purpose line in P1, P2
|
|
84
131
|
```
|
|
85
132
|
|
|
86
133
|
| Exit | Meaning |
|
|
87
134
|
| ---- | ------- |
|
|
88
|
-
| 0 | the contract is met (a missing dependency table
|
|
135
|
+
| 0 | the contract is met (a missing dependency table, a missing or stale head and a missing purpose line are notes, not gaps) |
|
|
89
136
|
| 1 | at least one part is missing or malformed |
|
|
90
137
|
| 2 | wrong command line: no subcommand, unknown subcommand, no file, unreadable file, unreadable config |
|
|
91
138
|
| 3 | the contract is met, but the plan or a board that links it is not on the base branch (a `base:` line names each path); the coordinator commits it first (`/orchestrate` § 2 step 0), after which the check counts as done |
|
package/install.sh
CHANGED
|
@@ -9,7 +9,7 @@ set -eu
|
|
|
9
9
|
|
|
10
10
|
# The default is the version this file ships with. The release bump changes it together with
|
|
11
11
|
# package.json; install.test.ts fails while the two differ. Overridable, but only by a plain release.
|
|
12
|
-
JACKHERD_VERSION="${JACKHERD_VERSION:-1.
|
|
12
|
+
JACKHERD_VERSION="${JACKHERD_VERSION:-1.26.0}"
|
|
13
13
|
|
|
14
14
|
JACKHERD_PACKAGE='jackherd'
|
|
15
15
|
# Test seam: the terminal the wizard asks its questions on.
|
|
@@ -59,11 +59,11 @@ const writeHistory = (main: string, entry: StandingHistoryEntry): boolean => {
|
|
|
59
59
|
try {
|
|
60
60
|
const location = boardLocationOf(readConfig([main]))
|
|
61
61
|
if (location === null) return false
|
|
62
|
-
const board = boardForLane(boardsIn(main, location), entry.lane, location.
|
|
62
|
+
const board = boardForLane(boardsIn(main, location), entry.lane, location.boardLanguage)
|
|
63
63
|
if (board === null) return false
|
|
64
64
|
const path = join(main, board.directory, location.board.file)
|
|
65
65
|
const text = readTextOrNull(path)
|
|
66
|
-
const next = text === null ? null : withHistoryRow(text, location.
|
|
66
|
+
const next = text === null ? null : withHistoryRow(text, location.boardLanguage, historyDate(entry.at), standingHistoryLine(entry))
|
|
67
67
|
if (next === null) return false
|
|
68
68
|
writeFileSync(path, next)
|
|
69
69
|
return true
|
|
@@ -47,5 +47,5 @@ const worktreeListOf = (main: string): string | undefined => {
|
|
|
47
47
|
export const planOfBranch = (main: string, settings: BoardSettings, branch: string): string | undefined => {
|
|
48
48
|
const list = worktreeListOf(main)
|
|
49
49
|
const lane = list === undefined ? undefined : laneInWorktreeList(list, main, settings.worktreeDirectory, branch)
|
|
50
|
-
return lane === undefined ? undefined : onlyPlanOf(boardsIn(main, settings), lane, settings.
|
|
50
|
+
return lane === undefined ? undefined : onlyPlanOf(boardsIn(main, settings), lane, settings.boardLanguage)
|
|
51
51
|
}
|