@sabaiway/agent-workflow-kit 12.0.0 → 12.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +2 -1
- package/SKILL.md +5 -1
- package/bridges/codex-cli-bridge/SKILL.md +14 -12
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +16 -15
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +9 -9
- package/bridges/codex-cli-bridge/bin/codex-review.sh +14 -13
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +8 -8
- package/bridges/codex-cli-bridge/capability.json +4 -4
- package/bridges/codex-cli-bridge/references/driving-codex.md +2 -2
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +6 -6
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bridge-settings.md +2 -2
- package/references/modes/now.md +30 -0
- package/tools/bridge-settings.mjs +1 -1
- package/tools/commands.mjs +7 -0
- package/tools/feedback-record.mjs +6 -2
- package/tools/now-cli.mjs +104 -0
- package/tools/now-facts.mjs +67 -0
- package/tools/now-plan-facts.mjs +129 -0
- package/tools/now-render.mjs +162 -0
- package/tools/now-status.mjs +145 -0
- package/tools/plan-shape-facts.mjs +1 -1
- package/tools/queue-audit-cli.mjs +17 -10
- package/tools/queue-audit-rows.mjs +98 -18
- package/tools/queue-audit.mjs +58 -12
- package/tools/queue-row-name.mjs +149 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The READABILITY judge of a backlog row: can the person who owns the queue read what the work is?
|
|
3
|
+
// The sibling question — is this row still WORK? — is queue-audit-rows.mjs, and the two never mix:
|
|
4
|
+
// this module classifies nothing and writes nothing. Governing contract:
|
|
5
|
+
// docs/ai/specs/kit/queue-row-name.md.
|
|
6
|
+
//
|
|
7
|
+
// Measured 2026-09-04 on this repo's own `## Pending / backlog`: 165 of 247 work-carrying rows
|
|
8
|
+
// failed this bar and 121 of those opened with the id itself, so every status answer named the work
|
|
9
|
+
// by machine ids the maintainer could not read. The fix is a rung, never a rule an agent remembers
|
|
10
|
+
// — the remembered rule is the discipline that already failed.
|
|
11
|
+
//
|
|
12
|
+
// Pure string functions. No IO, no argv, no side effects on import. Dependency-free, Node >= 22.
|
|
13
|
+
|
|
14
|
+
import { ROW_ID } from './feedback-record.mjs';
|
|
15
|
+
|
|
16
|
+
const IDENT = '[A-Za-z0-9_-]';
|
|
17
|
+
const PART = '[A-Z0-9]+';
|
|
18
|
+
const LETTERED = '(?=[A-Z0-9]*[A-Z])[A-Z0-9]+';
|
|
19
|
+
|
|
20
|
+
// AN ID IS A RUN THE ROW PRESENTS AS ONE — that is the whole rule, and it replaced "the first
|
|
21
|
+
// shouted kebab run anywhere", which was walked around from both sides at once: it cut a SENTENCE at
|
|
22
|
+
// its first shouted compound adjective (`… never a HAND-APPLY \`rm\` …` reported HAND-APPLY as the
|
|
23
|
+
// row's cross-reference key), and it matched NOTHING on the real ids this corpus writes, because one
|
|
24
|
+
// all-digit part killed the run — `UPGRADE-RUN-FEEDBACK-2026-08-18`, `FLOW-CHECK-64-HAS-NO-VALVE-…`
|
|
25
|
+
// and `AMEND-REACHES-FOR---NO-VERIFY` each handed the WHOLE title back as its "name", id included.
|
|
26
|
+
//
|
|
27
|
+
// TWO grammars, and the difference is who VOUCHES for the id:
|
|
28
|
+
//
|
|
29
|
+
// MARKED `id <ID>` — the row says so, so the id is exactly what a checked record may hand over:
|
|
30
|
+
// feedback-record.mjs's ROW_ID, imported rather than restated. `FOO`, `TASK-123` and
|
|
31
|
+
// `1-TASK` are ids here, and the kit's own row writer marks its ids for this reason.
|
|
32
|
+
// UNMARKED a heuristic over a display title nobody validated, so it stays narrow: parts of
|
|
33
|
+
// upper-case letters and digits joined by one or more hyphens (the corpus writes `---`),
|
|
34
|
+
// the FIRST TWO parts each carrying a letter, the rest unconstrained. That is what keeps
|
|
35
|
+
// `AD-133` a cross-reference inside a name rather than an id. A run SPANNING its whole
|
|
36
|
+
// segment needs only its first part lettered — nothing precedes it that it could eat.
|
|
37
|
+
//
|
|
38
|
+
// And the POSITION is where a row PRESENTS an id: the head of a ` — `-separated segment, or before a
|
|
39
|
+
// colon with nothing but WHITESPACE AND PUNCTUATION ahead of it in that segment. Both guards are
|
|
40
|
+
// measured: an id read mid-sentence took a claim's own words (`Preserve id TASK-123 in output — id
|
|
41
|
+
// ROW-A` answered TASK-123 rather than the row's id), and a prefix that admitted digits cut
|
|
42
|
+
// `123 ROW-A: the claim remains readable` down to the name `123`. What survives is why
|
|
43
|
+
// `See also FIRST-TIME-CORRECTNESS: …` keeps its name while `[ ] ROW-A: the claim` still names `[ ]`.
|
|
44
|
+
const RUN = `${LETTERED}(?:-+${LETTERED})(?:-+${PART})*`;
|
|
45
|
+
const SPANNING = `${LETTERED}(?:-+${PART})+`;
|
|
46
|
+
const MARKED = `id(?![A-Za-z0-9_])\\s+(${ROW_ID})(?!${IDENT})`;
|
|
47
|
+
const SEGMENT_SPLIT = /\s+[—–]\s+/u;
|
|
48
|
+
|
|
49
|
+
// Each form says where the NAME is cut: at the segment's own start for a presented head, at the
|
|
50
|
+
// `id` word for a marked id mid-sentence, and at the RUN itself for the label form — a cut at the
|
|
51
|
+
// match start there would swallow the `[ ]` the checkbox row is named by.
|
|
52
|
+
const FORMS = [
|
|
53
|
+
{ re: new RegExp(`^(?:${MARKED}|(${SPANNING}))$`, 'du'), cut: () => 0 },
|
|
54
|
+
{ re: new RegExp(`^(?:${MARKED}|(${RUN})(?!${IDENT}))`, 'du'), cut: () => 0 },
|
|
55
|
+
{ re: new RegExp(`^[\\s\\p{P}\\p{S}]*?(?<!${IDENT})(${RUN})(?=:)`, 'du'), cut: (match) => match.indices[1][0] },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const CASED = /\p{Cased}/u;
|
|
59
|
+
const LOWERCASE = /\p{Lowercase}/u;
|
|
60
|
+
const LETTER = /\p{L}/u;
|
|
61
|
+
const HYPHENS = /^-+$/u;
|
|
62
|
+
const normalize = (value) => String(value).replace(/-/gu, ' ').replace(/\s+/gu, ' ').trim().toLowerCase();
|
|
63
|
+
|
|
64
|
+
// nameOf(title) -> { name, id }. The title handed in is the DISPLAY text queue-audit already
|
|
65
|
+
// renders — every word the row wrote, its backticks stripped — with its leading STATUS segments
|
|
66
|
+
// already removed by the parser that owns the status vocabulary. Never the judged text, whose
|
|
67
|
+
// elided quotations would leave a HOLE where a quoted word stood and report an absent name for a
|
|
68
|
+
// row that reads perfectly. The boundary is stated POSITIVELY, by the id: ` — ` is the STATUS
|
|
69
|
+
// grammar's segment separator, not a name boundary, so a name that carries one keeps it whole, and
|
|
70
|
+
// a title that puts its id outside the bold span is named by that span entire.
|
|
71
|
+
export const nameOf = (title) => {
|
|
72
|
+
const display = String(title ?? '').trim();
|
|
73
|
+
let at = 0;
|
|
74
|
+
for (const text of display.split(SEGMENT_SPLIT)) {
|
|
75
|
+
const start = display.indexOf(text, at);
|
|
76
|
+
at = start + text.length;
|
|
77
|
+
for (const form of FORMS) {
|
|
78
|
+
const match = form.re.exec(text);
|
|
79
|
+
if (!match) continue;
|
|
80
|
+
const name = display.slice(0, start + form.cut(match)).trimEnd().replace(/(?:\s*[—–-]+)+$/u, '').trimEnd();
|
|
81
|
+
return { name, id: match[1] ?? match[2] };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { name: display, id: null };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ONE word list, and every clause below reads it. A hyphen-joined RUN is one word — `row-is-unreadable`
|
|
88
|
+
// is a slug, `fix---the---bug` is the same slug, and `2026-08-21` is one date rather than three
|
|
89
|
+
// numbers — while punctuation is no word at all, so the `[ ]` the kit's own row writer renders no
|
|
90
|
+
// longer supplies two of the three words the bar asks for. Word segmentation is the platform's
|
|
91
|
+
// (`Intl.Segmenter`, a JS built-in — this module stays dependency-free).
|
|
92
|
+
const WORDS = new Intl.Segmenter(undefined, { granularity: 'word' });
|
|
93
|
+
export const wordsOf = (text) => {
|
|
94
|
+
const parts = [...WORDS.segment(String(text))];
|
|
95
|
+
const words = [];
|
|
96
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
97
|
+
if (!parts[index].isWordLike) continue;
|
|
98
|
+
let back = index - 1;
|
|
99
|
+
while (back >= 0 && HYPHENS.test(parts[back].segment)) back -= 1;
|
|
100
|
+
if (back >= 0 && back < index - 1 && parts[back].isWordLike && words.length) {
|
|
101
|
+
words[words.length - 1] += parts.slice(back + 1, index + 1).map((part) => part.segment).join('');
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
words.push(parts[index].segment);
|
|
105
|
+
}
|
|
106
|
+
return words;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// A name is a SENTENCE, and that ONE invariant is the whole bar — never a table of shapes, because
|
|
110
|
+
// a table is walked around, and each earlier draft was: the slug written back in lower case walked
|
|
111
|
+
// around all four shapes the first draft enumerated; a dated parenthetical walked around a clause
|
|
112
|
+
// that asked the whole name for one lower-case letter anywhere; and "no cased character" as the
|
|
113
|
+
// signal for a caseless SCRIPT let a name of pure punctuation, digits or emoji (`[ ]`, `123`,
|
|
114
|
+
// `2026-09-04`) through as a sentence. So:
|
|
115
|
+
//
|
|
116
|
+
// a name CARRIES A LETTER — asked of every name, and the one clause a caseless script also meets;
|
|
117
|
+
// a name that carries CASE is three or more words, one of its first three CASED words carrying a
|
|
118
|
+
// lower-case letter.
|
|
119
|
+
//
|
|
120
|
+
// Binding the two counting clauses to case is what makes the verdict DETERMINISTIC rather than a
|
|
121
|
+
// function of the host's word-segmentation data: a caseless writing system writes no spaces to
|
|
122
|
+
// count and has no case to shout with, so neither is asked of it. Reading the SHOUTED clause off the
|
|
123
|
+
// first three CASED words (not the first three words) is what lets a Japanese sentence carry an
|
|
124
|
+
// English acronym without being called shouted.
|
|
125
|
+
export const nameFindings = (name, id) => {
|
|
126
|
+
const text = String(name ?? '').trim();
|
|
127
|
+
if (text === '') return [{ cause: 'absent', message: 'The row has no name before its id.' }];
|
|
128
|
+
const findings = [];
|
|
129
|
+
const words = wordsOf(text);
|
|
130
|
+
if (!words.some((word) => LETTER.test(word))) {
|
|
131
|
+
findings.push({ cause: 'no-letter', message: 'The name carries no letter, so it names nothing.' });
|
|
132
|
+
}
|
|
133
|
+
if (CASED.test(text) && words.length < 3) {
|
|
134
|
+
findings.push({ cause: 'too-few-words', message: 'The name is fewer than three words.' });
|
|
135
|
+
}
|
|
136
|
+
// The acronym exemption is for a MIXED-SCRIPT name — a caseless sentence carrying an English
|
|
137
|
+
// acronym — so it is spent only where caseless letter words actually exist. Skipping the clause
|
|
138
|
+
// on "fewer than three cased words" alone let `API 123 456` and `API GATE 2` through as sentences.
|
|
139
|
+
const cased = words.filter((word) => CASED.test(word));
|
|
140
|
+
const caseless = words.filter((word) => LETTER.test(word) && !CASED.test(word));
|
|
141
|
+
const exempt = caseless.length > 0 && cased.length < 3;
|
|
142
|
+
if (cased.length > 0 && !exempt && !cased.slice(0, 3).some((word) => LOWERCASE.test(word))) {
|
|
143
|
+
findings.push({ cause: 'shouted', message: 'None of the name\'s leading cased words carries a lower-case letter.' });
|
|
144
|
+
}
|
|
145
|
+
if (id !== null && normalize(text) === normalize(id)) {
|
|
146
|
+
findings.push({ cause: 'restates-the-id', message: 'The name only restates its id.' });
|
|
147
|
+
}
|
|
148
|
+
return findings;
|
|
149
|
+
};
|