@cluesmith/codev 3.3.4 → 3.3.5

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 (96) hide show
  1. package/dashboard-dist/assets/index-BuflRUi6.js +220 -0
  2. package/dashboard-dist/assets/index-BuflRUi6.js.map +1 -0
  3. package/dashboard-dist/assets/index-r3bZzLmX.css +32 -0
  4. package/dashboard-dist/index.html +2 -2
  5. package/dist/agent-farm/commands/send.d.ts +3 -2
  6. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  7. package/dist/agent-farm/commands/send.js +24 -5
  8. package/dist/agent-farm/commands/send.js.map +1 -1
  9. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  10. package/dist/agent-farm/commands/spawn-worktree.js +12 -2
  11. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  12. package/dist/agent-farm/servers/decisions/file.d.ts +50 -0
  13. package/dist/agent-farm/servers/decisions/file.d.ts.map +1 -0
  14. package/dist/agent-farm/servers/decisions/file.js +146 -0
  15. package/dist/agent-farm/servers/decisions/file.js.map +1 -0
  16. package/dist/agent-farm/servers/decisions/grammar.d.ts +36 -0
  17. package/dist/agent-farm/servers/decisions/grammar.d.ts.map +1 -0
  18. package/dist/agent-farm/servers/decisions/grammar.js +109 -0
  19. package/dist/agent-farm/servers/decisions/grammar.js.map +1 -0
  20. package/dist/agent-farm/servers/decisions/notice.d.ts +11 -0
  21. package/dist/agent-farm/servers/decisions/notice.d.ts.map +1 -0
  22. package/dist/agent-farm/servers/decisions/notice.js +18 -0
  23. package/dist/agent-farm/servers/decisions/notice.js.map +1 -0
  24. package/dist/agent-farm/servers/decisions/parse.d.ts +36 -0
  25. package/dist/agent-farm/servers/decisions/parse.d.ts.map +1 -0
  26. package/dist/agent-farm/servers/decisions/parse.js +308 -0
  27. package/dist/agent-farm/servers/decisions/parse.js.map +1 -0
  28. package/dist/agent-farm/servers/decisions/routes.d.ts +17 -0
  29. package/dist/agent-farm/servers/decisions/routes.d.ts.map +1 -0
  30. package/dist/agent-farm/servers/decisions/routes.js +160 -0
  31. package/dist/agent-farm/servers/decisions/routes.js.map +1 -0
  32. package/dist/agent-farm/servers/decisions/splice.d.ts +26 -0
  33. package/dist/agent-farm/servers/decisions/splice.d.ts.map +1 -0
  34. package/dist/agent-farm/servers/decisions/splice.js +120 -0
  35. package/dist/agent-farm/servers/decisions/splice.js.map +1 -0
  36. package/dist/agent-farm/servers/decisions/stamp.d.ts +4 -0
  37. package/dist/agent-farm/servers/decisions/stamp.d.ts.map +1 -0
  38. package/dist/agent-farm/servers/decisions/stamp.js +171 -0
  39. package/dist/agent-farm/servers/decisions/stamp.js.map +1 -0
  40. package/dist/agent-farm/servers/overview.d.ts +4 -3
  41. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  42. package/dist/agent-farm/servers/overview.js +51 -24
  43. package/dist/agent-farm/servers/overview.js.map +1 -1
  44. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  45. package/dist/agent-farm/servers/tower-routes.js +63 -5
  46. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  47. package/dist/agent-farm/utils/message-format.d.ts +8 -0
  48. package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
  49. package/dist/agent-farm/utils/message-format.js +16 -0
  50. package/dist/agent-farm/utils/message-format.js.map +1 -1
  51. package/dist/commands/consult/index.d.ts +20 -1
  52. package/dist/commands/consult/index.d.ts.map +1 -1
  53. package/dist/commands/consult/index.js +56 -2
  54. package/dist/commands/consult/index.js.map +1 -1
  55. package/dist/commands/porch/checks.d.ts +7 -1
  56. package/dist/commands/porch/checks.d.ts.map +1 -1
  57. package/dist/commands/porch/checks.js +77 -14
  58. package/dist/commands/porch/checks.js.map +1 -1
  59. package/dist/commands/porch/index.d.ts +38 -1
  60. package/dist/commands/porch/index.d.ts.map +1 -1
  61. package/dist/commands/porch/index.js +79 -43
  62. package/dist/commands/porch/index.js.map +1 -1
  63. package/dist/commands/porch/protocol.d.ts +4 -3
  64. package/dist/commands/porch/protocol.d.ts.map +1 -1
  65. package/dist/commands/porch/protocol.js +32 -5
  66. package/dist/commands/porch/protocol.js.map +1 -1
  67. package/dist/commands/porch/types.d.ts +2 -0
  68. package/dist/commands/porch/types.d.ts.map +1 -1
  69. package/dist/lib/agent-env.d.ts +10 -1
  70. package/dist/lib/agent-env.d.ts.map +1 -1
  71. package/dist/lib/agent-env.js +12 -1
  72. package/dist/lib/agent-env.js.map +1 -1
  73. package/dist/lib/config.d.ts +8 -0
  74. package/dist/lib/config.d.ts.map +1 -1
  75. package/dist/lib/config.js.map +1 -1
  76. package/package.json +4 -4
  77. package/skeleton/.claude/skills/afx/SKILL.md +15 -0
  78. package/skeleton/.claude/skills/arch-init/SKILL.md +56 -9
  79. package/skeleton/.claude/skills/arch-save/SKILL.md +100 -9
  80. package/skeleton/.claude/skills/consult/SKILL.md +9 -0
  81. package/skeleton/.claude/skills/porch/SKILL.md +18 -0
  82. package/skeleton/.codex/skills/afx/SKILL.md +15 -0
  83. package/skeleton/.codex/skills/arch-init/SKILL.md +56 -9
  84. package/skeleton/.codex/skills/arch-save/SKILL.md +100 -9
  85. package/skeleton/.codex/skills/consult/SKILL.md +9 -0
  86. package/skeleton/.codex/skills/porch/SKILL.md +18 -0
  87. package/skeleton/protocol-schema.json +6 -0
  88. package/skeleton/protocols/maintain/protocol.md +5 -0
  89. package/skeleton/protocols/pir/prompts/review.md +0 -1
  90. package/skeleton/protocols/protocol-schema.json +6 -0
  91. package/skeleton/resources/commands/agent-farm.md +44 -0
  92. package/skeleton/resources/commands/consult.md +9 -2
  93. package/skeleton/templates/cheatsheet.md +1 -1
  94. package/dashboard-dist/assets/index-DZf6pNPE.js +0 -199
  95. package/dashboard-dist/assets/index-DZf6pNPE.js.map +0 -1
  96. package/dashboard-dist/assets/index-DnYLy5C_.css +0 -32
@@ -0,0 +1,120 @@
1
+ // Ruling splice for the decisions file (Spec 1819).
2
+ //
3
+ // A pure function that writes the owner's ruling into the file's text, or refuses
4
+ // with a specific reason and returns no text. The write route is a thin wrapper
5
+ // around it: read the file, splice, write back only on success.
6
+ //
7
+ // The ruling goes directly after the item's `**Your word:**` line: the word on the
8
+ // very next line; then the comment, after a blank line when a word was written; then
9
+ // a blank line when the line that follows is not already blank. Inserted lines use
10
+ // the file's own line ending, a missing final newline on the Your-word line is
11
+ // supplied, and every other character is preserved in order. The result is
12
+ // re-parsed and must keep the file's structure — anything the explicit checks miss
13
+ // (an option word that is itself a heading or fence, say) is `structure-changed`.
14
+ import { isFenceLine, isHeadingLine, isYourWordLine } from './grammar.js';
15
+ import { parseDecisions } from './parse.js';
16
+ /** Longest comment accepted, in characters, after trimming. */
17
+ export const MAX_COMMENT_LENGTH = 10_000;
18
+ function refuse(code, message) {
19
+ return { ok: false, code, message };
20
+ }
21
+ /** Refuse a comment line that would add structure: a heading, fence or Your-word line. */
22
+ function checkCommentLines(commentLines) {
23
+ for (const [n, line] of commentLines.entries()) {
24
+ const where = `Line ${n + 1} of the comment`;
25
+ if (isHeadingLine(line))
26
+ return refuse('comment-heading', `${where} is a markdown heading ("${line.trim()}"); headings end the item.`);
27
+ if (isFenceLine(line))
28
+ return refuse('comment-fence', `${where} is a code fence ("${line.trim()}"), which would change how the file parses.`);
29
+ if (isYourWordLine(line))
30
+ return refuse('comment-your-word', `${where} starts with **Your word:**, which would give the item a second Your-word line.`);
31
+ }
32
+ return null;
33
+ }
34
+ /** Find the request's item in `## Open` and check it can take a ruling now. */
35
+ function checkItem(parsed, request, word) {
36
+ const { id } = request;
37
+ if (parsed.problems.includes('open-multiple')) {
38
+ return refuse('open-multiple', 'The file has more than one ## Open section; the tab cannot tell where items belong, so it writes nothing.');
39
+ }
40
+ const matches = parsed.items.filter(item => item.id === id);
41
+ if (matches.length === 0)
42
+ return refuse('heading-missing', `Item ${id} is not in ## Open (it may have been ruled or removed).`);
43
+ if (matches.length > 1)
44
+ return refuse('heading-duplicate', `Item ${id} appears ${matches.length} times in ## Open; rulings for it go through main.`);
45
+ const item = matches[0];
46
+ if (item.yourWordLines.length === 0)
47
+ return refuse('your-word-missing', `Item ${id} has no **Your word:** line to rule under.`);
48
+ if (item.yourWordLines.length > 1)
49
+ return refuse('your-word-multiple', `Item ${id} has ${item.yourWordLines.length} **Your word:** lines; the tab cannot tell which one to rule under.`);
50
+ if (item.currentRuling !== '')
51
+ return refuse('already-ruled', `Item ${id} already has text under its **Your word:** line; a change goes through main or a hand edit.`);
52
+ if (word && !item.options.some(option => option.word === word)) {
53
+ return refuse('unknown-word', `"${word}" is not an option on item ${id}'s current **Your word:** line.`);
54
+ }
55
+ if (item.fingerprint !== request.fingerprint)
56
+ return refuse('item-changed', `Item ${id} changed since it was shown; review it and submit again.`);
57
+ return item;
58
+ }
59
+ /**
60
+ * Check the spliced text kept the file's structure: the same sections, the same items
61
+ * in the same order, the same options on this item, this item's current ruling equal
62
+ * to the inserted ruling, and every inserted line plain unfenced content.
63
+ */
64
+ function verify(before, after, item, ruling, insertedLines) {
65
+ const sections = (p) => p.lines.filter(l => l.headingLevel === 1 || l.headingLevel === 2).map(l => `${l.headingLevel} ${l.headingText}`).join('\n');
66
+ const changed = (what) => refuse('structure-changed', `Writing this ruling into item ${item.id} would change ${what}, so nothing was written.`);
67
+ if (sections(after) !== sections(before) || after.problems.join() !== before.problems.join())
68
+ return changed("the file's sections");
69
+ if (after.items.map(i => i.id).join('\n') !== before.items.map(i => i.id).join('\n'))
70
+ return changed('the list of open items');
71
+ const afterItem = after.items.find(i => i.headingLine === item.headingLine);
72
+ if (!afterItem || afterItem.id !== item.id)
73
+ return changed('the list of open items');
74
+ if (JSON.stringify(afterItem.options) !== JSON.stringify(item.options))
75
+ return changed("the item's options");
76
+ if (afterItem.currentRuling !== ruling)
77
+ return changed("the item's text after its **Your word:** line");
78
+ const inserted = after.lines.slice(insertedLines.from, insertedLines.from + insertedLines.count);
79
+ if (inserted.some(l => l.fenced || l.headingLevel > 0 || l.yourWord))
80
+ return changed("the file's structure (the ruling would add a heading or fence)");
81
+ return null;
82
+ }
83
+ /** Apply the owner's ruling to the decisions file's text, or refuse with the reason. */
84
+ export function spliceRuling(text, request) {
85
+ if (!request.id)
86
+ return refuse('invalid-request', 'The request names no item.');
87
+ if (!request.fingerprint)
88
+ return refuse('invalid-request', 'The request carries no item fingerprint.');
89
+ const word = request.word ?? '';
90
+ const comment = (request.comment ?? '').trim();
91
+ if (!word && !comment)
92
+ return refuse('empty-ruling', 'Choose an option or write a comment.');
93
+ if (comment.length > MAX_COMMENT_LENGTH) {
94
+ return refuse('comment-too-long', `The comment is ${comment.length} characters; the limit is ${MAX_COMMENT_LENGTH}.`);
95
+ }
96
+ const commentLines = comment ? comment.split(/\r\n|\r|\n/) : [];
97
+ const commentRefusal = checkCommentLines(commentLines);
98
+ if (commentRefusal)
99
+ return commentRefusal;
100
+ const before = parseDecisions(text);
101
+ const item = checkItem(before, request, word);
102
+ if ('ok' in item)
103
+ return item;
104
+ const rulingLines = [...(word ? [word] : []), ...(word && comment ? [''] : []), ...commentLines];
105
+ const yourWord = before.lines[item.yourWordLines[0]];
106
+ const following = before.lines[yourWord.index + 1];
107
+ const newLines = following && following.text.trim() !== '' ? [...rulingLines, ''] : rulingLines;
108
+ const at = yourWord.end + yourWord.eol.length;
109
+ const inserted = (yourWord.eol === '' ? before.eol : '') + newLines.map(line => line + before.eol).join('');
110
+ const result = text.slice(0, at) + inserted + text.slice(at);
111
+ // The parser's current ruling is LF-joined whatever the file's line ending.
112
+ const refusal = verify(before, parseDecisions(result), item, rulingLines.join('\n'), {
113
+ from: yourWord.index + 1,
114
+ count: newLines.length,
115
+ });
116
+ if (refusal)
117
+ return refusal;
118
+ return { ok: true, text: result };
119
+ }
120
+ //# sourceMappingURL=splice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splice.js","sourceRoot":"","sources":["../../../../src/agent-farm/servers/decisions/splice.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,EAAE;AACF,kFAAkF;AAClF,gFAAgF;AAChF,gEAAgE;AAChE,EAAE;AACF,mFAAmF;AACnF,qFAAqF;AACrF,mFAAmF;AACnF,+EAA+E;AAC/E,2EAA2E;AAC3E,mFAAmF;AACnF,kFAAkF;AAGlF,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAyC,MAAM,YAAY,CAAC;AAEnF,+DAA+D;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAuBzC,SAAS,MAAM,CAAC,IAAuB,EAAE,OAAe;IACtD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AACtC,CAAC;AAED,0FAA0F;AAC1F,SAAS,iBAAiB,CAAC,YAAsB;IAC/C,KAAK,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC;QAC7C,IAAI,aAAa,CAAC,IAAI,CAAC;YAAE,OAAO,MAAM,CAAC,iBAAiB,EAAE,GAAG,KAAK,4BAA4B,IAAI,CAAC,IAAI,EAAE,4BAA4B,CAAC,CAAC;QACvI,IAAI,WAAW,CAAC,IAAI,CAAC;YAAE,OAAO,MAAM,CAAC,eAAe,EAAE,GAAG,KAAK,sBAAsB,IAAI,CAAC,IAAI,EAAE,6CAA6C,CAAC,CAAC;QAC9I,IAAI,cAAc,CAAC,IAAI,CAAC;YAAE,OAAO,MAAM,CAAC,mBAAmB,EAAE,GAAG,KAAK,iFAAiF,CAAC,CAAC;IAC1J,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,+EAA+E;AAC/E,SAAS,SAAS,CAAC,MAAuB,EAAE,OAAsB,EAAE,IAAY;IAC9E,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC;IACvB,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;QAC9C,OAAO,MAAM,CAAC,eAAe,EAAE,2GAA2G,CAAC,CAAC;IAC9I,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,iBAAiB,EAAE,QAAQ,EAAE,yDAAyD,CAAC,CAAC;IAChI,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC,mBAAmB,EAAE,QAAQ,EAAE,YAAY,OAAO,CAAC,MAAM,oDAAoD,CAAC,CAAC;IACrJ,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,mBAAmB,EAAE,QAAQ,EAAE,4CAA4C,CAAC,CAAC;IAChI,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC,oBAAoB,EAAE,QAAQ,EAAE,QAAQ,IAAI,CAAC,aAAa,CAAC,MAAM,qEAAqE,CAAC,CAAC;IACzL,IAAI,IAAI,CAAC,aAAa,KAAK,EAAE;QAAE,OAAO,MAAM,CAAC,eAAe,EAAE,QAAQ,EAAE,6FAA6F,CAAC,CAAC;IACvK,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QAC/D,OAAO,MAAM,CAAC,cAAc,EAAE,IAAI,IAAI,8BAA8B,EAAE,iCAAiC,CAAC,CAAC;IAC3G,CAAC;IACD,IAAI,IAAI,CAAC,WAAW,KAAK,OAAO,CAAC,WAAW;QAAE,OAAO,MAAM,CAAC,cAAc,EAAE,QAAQ,EAAE,0DAA0D,CAAC,CAAC;IAClJ,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAS,MAAM,CACb,MAAuB,EACvB,KAAsB,EACtB,IAAgB,EAChB,MAAc,EACd,aAA8C;IAE9C,MAAM,QAAQ,GAAG,CAAC,CAAkB,EAAE,EAAE,CACtC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,CAAC,IAAI,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9H,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,EAAE,CAC/B,MAAM,CAAC,mBAAmB,EAAE,iCAAiC,IAAI,CAAC,EAAE,iBAAiB,IAAI,2BAA2B,CAAC,CAAC;IAExH,IAAI,QAAQ,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;QAAE,OAAO,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACpI,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAE/H,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,IAAI,CAAC,WAAW,CAAC,CAAC;IAC5E,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE;QAAE,OAAO,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACrF,IAAI,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC7G,IAAI,SAAS,CAAC,aAAa,KAAK,MAAM;QAAE,OAAO,OAAO,CAAC,+CAA+C,CAAC,CAAC;IAExG,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IACjG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,YAAY,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;QAAE,OAAO,OAAO,CAAC,gEAAgE,CAAC,CAAC;IACvJ,OAAO,IAAI,CAAC;AACd,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,OAAsB;IAC/D,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC,iBAAiB,EAAE,4BAA4B,CAAC,CAAC;IAChF,IAAI,CAAC,OAAO,CAAC,WAAW;QAAE,OAAO,MAAM,CAAC,iBAAiB,EAAE,0CAA0C,CAAC,CAAC;IAEvG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/C,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO;QAAE,OAAO,MAAM,CAAC,cAAc,EAAE,sCAAsC,CAAC,CAAC;IAC7F,IAAI,OAAO,CAAC,MAAM,GAAG,kBAAkB,EAAE,CAAC;QACxC,OAAO,MAAM,CAAC,kBAAkB,EAAE,kBAAkB,OAAO,CAAC,MAAM,6BAA6B,kBAAkB,GAAG,CAAC,CAAC;IACxH,CAAC;IACD,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,MAAM,cAAc,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;IACvD,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAE1C,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC9C,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAE9B,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC;IACjG,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,SAAS,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;IAEhG,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC;IAC9C,MAAM,QAAQ,GAAG,CAAC,QAAQ,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5G,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAE7D,4EAA4E;IAC5E,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACnF,IAAI,EAAE,QAAQ,CAAC,KAAK,GAAG,CAAC;QACxB,KAAK,EAAE,QAAQ,CAAC,MAAM;KACvB,CAAC,CAAC;IACH,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AACpC,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { DecisionStamp } from '@cluesmith/codev-types';
2
+ /** Resolve a heading stamp to an instant using only the zone written in it. */
3
+ export declare function resolveStamp(raw: string): DecisionStamp;
4
+ //# sourceMappingURL=stamp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stamp.d.ts","sourceRoot":"","sources":["../../../../src/agent-farm/servers/decisions/stamp.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AA2H5D,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAuCvD"}
@@ -0,0 +1,171 @@
1
+ // Decision heading timestamps (Spec 1819).
2
+ //
3
+ // A stamp is `YYYY-MM-DD h:mm am|pm <ZONE>` or `YYYY-MM-DD HH:mm <ZONE>`. It is
4
+ // resolved to an absolute instant using ONLY the zone written in it — nothing here
5
+ // defaults to, or special-cases, any zone. Anything not in the accepted forms is
6
+ // returned unconverted with a reason, so the tab shows it verbatim.
7
+ //
8
+ // IANA and generic zones are resolved with the runtime's built-in `Intl` time-zone
9
+ // data by offset probing: the wall-clock time is tried against every offset the
10
+ // zone has near that date, and the candidates that round-trip are kept. One means
11
+ // success; none is a spring-forward gap; two is a fall-back fold. Gaps and folds
12
+ // are left unconverted rather than broken by a tie-break rule.
13
+ /** Generic North-American zones: DST follows the stamp's date. */
14
+ const GENERIC_ZONES = {
15
+ PT: 'America/Los_Angeles',
16
+ MT: 'America/Denver',
17
+ CT: 'America/Chicago',
18
+ ET: 'America/New_York',
19
+ AKT: 'America/Anchorage',
20
+ HT: 'Pacific/Honolulu',
21
+ };
22
+ /**
23
+ * Fixed-offset abbreviations, in minutes east of UTC. `CST` is North-American
24
+ * Central (China Standard Time is written `+08:00` or `Asia/Shanghai`). Ambiguous
25
+ * abbreviations (`IST`, `BST`, `AST`, …) are deliberately absent.
26
+ */
27
+ const FIXED_ZONES = {
28
+ UTC: 0,
29
+ GMT: 0,
30
+ Z: 0,
31
+ PST: -8 * 60,
32
+ PDT: -7 * 60,
33
+ MST: -7 * 60,
34
+ MDT: -6 * 60,
35
+ CST: -6 * 60,
36
+ CDT: -5 * 60,
37
+ EST: -5 * 60,
38
+ EDT: -4 * 60,
39
+ AKST: -9 * 60,
40
+ AKDT: -8 * 60,
41
+ HST: -10 * 60,
42
+ WET: 0,
43
+ WEST: 1 * 60,
44
+ CET: 1 * 60,
45
+ CEST: 2 * 60,
46
+ EET: 2 * 60,
47
+ EEST: 3 * 60,
48
+ MSK: 3 * 60,
49
+ SGT: 8 * 60,
50
+ HKT: 8 * 60,
51
+ JST: 9 * 60,
52
+ KST: 9 * 60,
53
+ AEST: 10 * 60,
54
+ AEDT: 11 * 60,
55
+ NZST: 12 * 60,
56
+ NZDT: 13 * 60,
57
+ };
58
+ const STAMP_RE = /^(\d{4})-(\d{2})-(\d{2})\s+(\d{1,2}):(\d{2})(?:\s*(am|pm))?\s+(\S.*?)\s*$/i;
59
+ const OFFSET_RE = /^(?:UTC|GMT)?([+-])(\d{1,2})(?::?(\d{2}))?$/;
60
+ const MINUTE_MS = 60_000;
61
+ const DAY_MS = 24 * 60 * MINUTE_MS;
62
+ function isIanaZone(name) {
63
+ try {
64
+ new Intl.DateTimeFormat('en-US', { timeZone: name });
65
+ return true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ function parseZone(zone) {
72
+ // Own keys only: `in` would let prototype names (`toString`, `__proto__`, …) through.
73
+ if (Object.hasOwn(FIXED_ZONES, zone))
74
+ return { kind: 'fixed', offsetMinutes: FIXED_ZONES[zone] };
75
+ if (Object.hasOwn(GENERIC_ZONES, zone))
76
+ return { kind: 'iana', name: GENERIC_ZONES[zone] };
77
+ const offset = OFFSET_RE.exec(zone);
78
+ if (offset) {
79
+ const hours = Number(offset[2]);
80
+ const minutes = offset[3] ? Number(offset[3]) : 0;
81
+ if (hours > 14 || minutes > 59)
82
+ return null;
83
+ const sign = offset[1] === '-' ? -1 : 1;
84
+ return { kind: 'fixed', offsetMinutes: sign * (hours * 60 + minutes) };
85
+ }
86
+ // IANA names only in Area/Location form, so legacy ids (`EST5EDT`, `IST`, …) are
87
+ // never accepted through the back door.
88
+ if (zone.includes('/') && isIanaZone(zone))
89
+ return { kind: 'iana', name: zone };
90
+ return null;
91
+ }
92
+ const formatters = new Map();
93
+ function formatterFor(zone) {
94
+ let f = formatters.get(zone);
95
+ if (!f) {
96
+ f = new Intl.DateTimeFormat('en-US', {
97
+ timeZone: zone,
98
+ hourCycle: 'h23',
99
+ year: 'numeric',
100
+ month: '2-digit',
101
+ day: '2-digit',
102
+ hour: '2-digit',
103
+ minute: '2-digit',
104
+ });
105
+ formatters.set(zone, f);
106
+ }
107
+ return f;
108
+ }
109
+ /** The zone's UTC offset, in minutes, at the given instant. */
110
+ function offsetAt(instantMs, zone) {
111
+ const parts = formatterFor(zone).formatToParts(new Date(instantMs));
112
+ const get = (type) => Number(parts.find(p => p.type === type).value);
113
+ const wallMs = Date.UTC(get('year'), get('month') - 1, get('day'), get('hour'), get('minute'));
114
+ const instantMinute = Math.floor(instantMs / MINUTE_MS) * MINUTE_MS;
115
+ return Math.round((wallMs - instantMinute) / MINUTE_MS);
116
+ }
117
+ /** All instants whose wall-clock time in `zone` is `wallMs` (as if UTC). */
118
+ function instantsForWallClock(wallMs, zone) {
119
+ const offsets = new Set([offsetAt(wallMs - DAY_MS, zone), offsetAt(wallMs, zone), offsetAt(wallMs + DAY_MS, zone)]);
120
+ const instants = new Set();
121
+ for (const offset of offsets) {
122
+ const candidate = wallMs - offset * MINUTE_MS;
123
+ if (offsetAt(candidate, zone) === offset)
124
+ instants.add(candidate);
125
+ }
126
+ return [...instants].sort((a, b) => a - b);
127
+ }
128
+ /** Resolve a heading stamp to an instant using only the zone written in it. */
129
+ export function resolveStamp(raw) {
130
+ const m = STAMP_RE.exec(raw.trim());
131
+ if (!m)
132
+ return { raw, instant: null, unconvertedReason: 'unrecognized-format' };
133
+ const [, y, mo, d, h, mi, meridiem, zoneText] = m;
134
+ // `12:10 am` with no zone backtracks to read `am` as the zone; it has none.
135
+ if (/^(am|pm)$/i.test(zoneText))
136
+ return { raw, instant: null, unconvertedReason: 'unrecognized-format' };
137
+ const year = Number(y);
138
+ const month = Number(mo);
139
+ const day = Number(d);
140
+ let hour = Number(h);
141
+ const minute = Number(mi);
142
+ if (meridiem) {
143
+ if (hour < 1 || hour > 12)
144
+ return { raw, instant: null, unconvertedReason: 'unrecognized-format' };
145
+ const pm = meridiem.toLowerCase() === 'pm';
146
+ hour = (hour % 12) + (pm ? 12 : 0);
147
+ }
148
+ else if (hour > 23) {
149
+ return { raw, instant: null, unconvertedReason: 'unrecognized-format' };
150
+ }
151
+ if (minute > 59)
152
+ return { raw, instant: null, unconvertedReason: 'unrecognized-format' };
153
+ const wallMs = Date.UTC(year, month - 1, day, hour, minute);
154
+ const check = new Date(wallMs);
155
+ if (check.getUTCFullYear() !== year || check.getUTCMonth() !== month - 1 || check.getUTCDate() !== day) {
156
+ return { raw, instant: null, unconvertedReason: 'unrecognized-format' };
157
+ }
158
+ const zone = parseZone(zoneText);
159
+ if (!zone)
160
+ return { raw, instant: null, unconvertedReason: 'unknown-zone' };
161
+ if (zone.kind === 'fixed') {
162
+ return { raw, instant: new Date(wallMs - zone.offsetMinutes * MINUTE_MS).toISOString() };
163
+ }
164
+ const instants = instantsForWallClock(wallMs, zone.name);
165
+ if (instants.length === 0)
166
+ return { raw, instant: null, unconvertedReason: 'nonexistent-time' };
167
+ if (instants.length > 1)
168
+ return { raw, instant: null, unconvertedReason: 'ambiguous-time' };
169
+ return { raw, instant: new Date(instants[0]).toISOString() };
170
+ }
171
+ //# sourceMappingURL=stamp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stamp.js","sourceRoot":"","sources":["../../../../src/agent-farm/servers/decisions/stamp.ts"],"names":[],"mappings":"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"}
@@ -58,6 +58,7 @@ export declare function calculateProgress(parsed: ParsedStatus, workspaceRoot?:
58
58
  * Each phase gets an equal share of 100%, with 'verified'/'complete' always = 100.
59
59
  */
60
60
  export declare function calculateEvenProgress(phase: string, phases: string[]): number;
61
+ /** Human-readable label for the gate the builder is blocked on, or null. */
61
62
  export declare function detectBlocked(parsed: ParsedStatus): string | null;
62
63
  /**
63
64
  * Canonical gate name (e.g. "plan-approval") for the gate the builder is
@@ -69,9 +70,9 @@ export declare function detectBlockedGate(parsed: ParsedStatus): string | null;
69
70
  * Detect when the current blocked gate was first requested.
70
71
  * Returns the ISO timestamp string or null if not blocked.
71
72
  *
72
- * Iterates `GATE_LABELS` so the gate set lives in exactly one place — sibling
73
- * to `detectBlocked` / `detectBlockedGate` (#927 removed this function's
74
- * previously-separate hardcoded array, which was a silent drift hazard).
73
+ * Shares `findBlockedGate` with `detectBlocked` / `detectBlockedGate` so the
74
+ * gate set lives in exactly one place (#927 removed a previously-separate
75
+ * hardcoded array, which was a silent drift hazard).
75
76
  */
76
77
  export declare function detectBlockedSince(parsed: ParsedStatus): string | null;
77
78
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/overview.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/overview.ts"],"names":[],"mappings":"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"}
@@ -280,13 +280,17 @@ function loadProtocolPhases(workspaceRoot, protocolName) {
280
280
  }
281
281
  /**
282
282
  * Detect if a builder is blocked on a gate (requested but not approved).
283
- * Returns a human-readable label or null.
284
283
  *
285
- * The allowlist mirrors the gates emitted by the bundled protocols (SPIR,
286
- * ASPIR, BUGFIX, AIR, PIR). New protocols that introduce new gate names must
287
- * register them here, otherwise their gate-pending state is invisible to
288
- * `OverviewBuilder.blocked` and downstream UIs (VSCode Needs Attention tree,
289
- * VSCode toast, dashboard NeedsAttentionList, status bar counter).
284
+ * Generic over gate names (#1777): ANY gate in `status.yaml` with
285
+ * `status: pending` and a `requested_at` timestamp is a human gate porch is
286
+ * holding the lane at, so it surfaces on `OverviewBuilder.blocked` and the
287
+ * downstream UIs (VSCode Agents tree/toast/status bar, dashboard
288
+ * NeedsAttentionList, Tower hub attention rows) whatever protocol emitted it.
289
+ * The earlier allowlist made EXPERIMENT's `experiment-complete` (and
290
+ * MAINTAIN/RESEARCH's gates) invisible.
291
+ *
292
+ * `GATE_LABELS` supplies display labels and the scan priority for the bundled
293
+ * gates; an unlisted gate is still detected, labelled from its own name.
290
294
  */
291
295
  const GATE_LABELS = {
292
296
  'spec-approval': 'spec review',
@@ -299,43 +303,57 @@ const GATE_LABELS = {
299
303
  // bar). The `pr` gate stays mapped here too (VSCode depends on it); the
300
304
  // dashboard alone excludes `pr` from its gate rows since it renders PR rows.
301
305
  'verify-approval': 'verify review',
306
+ 'experiment-complete': 'experiment review',
307
+ 'maintain-complete': 'maintenance review',
308
+ 'scope-approval': 'scope review',
309
+ 'research-complete': 'research review',
302
310
  };
303
- export function detectBlocked(parsed) {
304
- for (const [gate, label] of Object.entries(GATE_LABELS)) {
311
+ /** Display label for a gate: the mapped label, else derived from its name. */
312
+ function gateLabel(gate) {
313
+ return GATE_LABELS[gate] ?? `${gate.replace(/-(approval|complete|review)$/, '').replace(/-/g, ' ')} review`;
314
+ }
315
+ /**
316
+ * Canonical name of the gate the builder is blocked on, or null. Known gates
317
+ * are scanned first (preserving their priority order), then any other gate in
318
+ * `status.yaml` order.
319
+ */
320
+ function findBlockedGate(parsed) {
321
+ const gates = new Set([...Object.keys(GATE_LABELS), ...Object.keys(parsed.gates)]);
322
+ for (const gate of gates) {
305
323
  if (parsed.gates[gate] === 'pending' && parsed.gateRequestedAt[gate]) {
306
- return label;
324
+ return gate;
307
325
  }
308
326
  }
309
327
  return null;
310
328
  }
329
+ /** Human-readable label for the gate the builder is blocked on, or null. */
330
+ export function detectBlocked(parsed) {
331
+ const gate = findBlockedGate(parsed);
332
+ if (!gate)
333
+ return null;
334
+ return gateLabel(gate);
335
+ }
311
336
  /**
312
337
  * Canonical gate name (e.g. "plan-approval") for the gate the builder is
313
338
  * blocked on. Sibling to `detectBlocked` which returns the display label.
314
339
  * Returns null if the builder isn't blocked.
315
340
  */
316
341
  export function detectBlockedGate(parsed) {
317
- for (const gate of Object.keys(GATE_LABELS)) {
318
- if (parsed.gates[gate] === 'pending' && parsed.gateRequestedAt[gate]) {
319
- return gate;
320
- }
321
- }
322
- return null;
342
+ return findBlockedGate(parsed);
323
343
  }
324
344
  /**
325
345
  * Detect when the current blocked gate was first requested.
326
346
  * Returns the ISO timestamp string or null if not blocked.
327
347
  *
328
- * Iterates `GATE_LABELS` so the gate set lives in exactly one place — sibling
329
- * to `detectBlocked` / `detectBlockedGate` (#927 removed this function's
330
- * previously-separate hardcoded array, which was a silent drift hazard).
348
+ * Shares `findBlockedGate` with `detectBlocked` / `detectBlockedGate` so the
349
+ * gate set lives in exactly one place (#927 removed a previously-separate
350
+ * hardcoded array, which was a silent drift hazard).
331
351
  */
332
352
  export function detectBlockedSince(parsed) {
333
- for (const gate of Object.keys(GATE_LABELS)) {
334
- if (parsed.gates[gate] === 'pending' && parsed.gateRequestedAt[gate]) {
335
- return parsed.gateRequestedAt[gate];
336
- }
337
- }
338
- return null;
353
+ const gate = findBlockedGate(parsed);
354
+ if (!gate)
355
+ return null;
356
+ return parsed.gateRequestedAt[gate];
339
357
  }
340
358
  /**
341
359
  * Compute the canonical "PR ready for human review" signal for a builder.
@@ -483,6 +501,15 @@ export function extractProjectIdFromWorktreeName(dirName) {
483
501
  const pirMatch = dirName.match(/^pir-(\d+)/);
484
502
  if (pirMatch)
485
503
  return pirMatch[1];
504
+ // task-NAvW, worktree-foIg → null (soft mode). Checked before the generic
505
+ // case below because a short id can be all digits (task-1234).
506
+ if (/^(task|worktree)-/.test(dirName))
507
+ return null;
508
+ // Any other numbered protocol spawn (experiment/maintain/research, #1777):
509
+ // experiment-1560 → "1560" (porch project dir is 1560-slug, like SPIR).
510
+ const genericMatch = dirName.match(/^[a-z]+-(\d+)(?:-|$)/);
511
+ if (genericMatch)
512
+ return genericMatch[1];
486
513
  // Legacy numeric: 0110 or 0110-slug → "0110"
487
514
  const numericMatch = dirName.match(/^(\d+)(?:-|$)/);
488
515
  if (numericMatch)