@sabaiway/agent-workflow-kit 14.5.0 → 14.6.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 +40 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/upgrade.md +11 -7
- package/references/shared/composition-handoff.md +6 -4
- package/references/templates/agent_rules.md +3 -0
- package/tools/delegation.mjs +2 -2
- package/tools/lens-region.mjs +186 -308
- package/tools/profile-gaps.mjs +90 -0
- package/tools/rules-insert.mjs +203 -0
- package/tools/rules-regions.mjs +167 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import {
|
|
4
|
+
RULES_REGIONS,
|
|
5
|
+
extractRegionBy,
|
|
6
|
+
readTemplateSpan,
|
|
7
|
+
planInsert,
|
|
8
|
+
frontmatterMaxLines,
|
|
9
|
+
decodeUtf8Strict,
|
|
10
|
+
} from './rules-regions.mjs';
|
|
11
|
+
|
|
12
|
+
const GAP_ID = 'story-sessions-section';
|
|
13
|
+
const STORY_ID = 'story-sessions';
|
|
14
|
+
const TEMPLATE_CANON = 'template';
|
|
15
|
+
const ENCODING = 'utf8';
|
|
16
|
+
const MISSING_FILE_CODE = 'ENOENT';
|
|
17
|
+
const SINGLE_HEADING = 1;
|
|
18
|
+
const TARGET_PATH = 'docs/ai/agent_rules.md';
|
|
19
|
+
const TEMPLATE_PATH = fileURLToPath(new URL('../references/templates/agent_rules.md', import.meta.url));
|
|
20
|
+
const INSERT_PATH = fileURLToPath(new URL('./rules-insert.mjs', import.meta.url));
|
|
21
|
+
const COMMAND_PREFIX = 'node ';
|
|
22
|
+
const CWD_FLAG = ' --cwd ';
|
|
23
|
+
const VERDICTS = Object.freeze({
|
|
24
|
+
present: 'present',
|
|
25
|
+
absent: 'absent',
|
|
26
|
+
undecidable: 'undecidable',
|
|
27
|
+
});
|
|
28
|
+
const REASONS = Object.freeze({
|
|
29
|
+
template: 'template',
|
|
30
|
+
fileAbsent: 'file-absent',
|
|
31
|
+
fileSymlink: 'file-symlink',
|
|
32
|
+
fileUnreadable: 'file-unreadable',
|
|
33
|
+
});
|
|
34
|
+
const TEMPLATE_REGIONS = RULES_REGIONS.filter(({ canon }) => canon === TEMPLATE_CANON);
|
|
35
|
+
const STORY_REGION = RULES_REGIONS.find(({ id }) => id === STORY_ID);
|
|
36
|
+
|
|
37
|
+
const reportUndecidable = (reason) => ({ verdict: VERDICTS.undecidable, reason });
|
|
38
|
+
|
|
39
|
+
const readSpans = (deps) => {
|
|
40
|
+
try {
|
|
41
|
+
const template = deps.readFileSync(TEMPLATE_PATH, ENCODING);
|
|
42
|
+
const entries = TEMPLATE_REGIONS.map(({ id, headingRe }) => [
|
|
43
|
+
id, readTemplateSpan(template, headingRe),
|
|
44
|
+
]);
|
|
45
|
+
const spans = Object.fromEntries(entries);
|
|
46
|
+
if (Object.values(spans).some(({ defect }) => defect)) {
|
|
47
|
+
return reportUndecidable(REASONS.template);
|
|
48
|
+
}
|
|
49
|
+
return { spans };
|
|
50
|
+
} catch {
|
|
51
|
+
return reportUndecidable(REASONS.template);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const readTarget = (root, deps) => {
|
|
56
|
+
const target = resolve(root, TARGET_PATH);
|
|
57
|
+
try {
|
|
58
|
+
const stat = deps.lstatSync(target);
|
|
59
|
+
if (stat.isSymbolicLink()) return reportUndecidable(REASONS.fileSymlink);
|
|
60
|
+
if (!stat.isFile()) return reportUndecidable(REASONS.fileUnreadable);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const reason = error?.code === MISSING_FILE_CODE ? REASONS.fileAbsent : REASONS.fileUnreadable;
|
|
63
|
+
return reportUndecidable(reason);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return { text: decodeUtf8Strict(deps.readFileSync(target)) };
|
|
67
|
+
} catch {
|
|
68
|
+
return reportUndecidable(REASONS.fileUnreadable);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const detectStorySessions = ({ root, deps }) => {
|
|
73
|
+
const template = readSpans(deps);
|
|
74
|
+
if (template.verdict) return template;
|
|
75
|
+
const target = readTarget(root, deps);
|
|
76
|
+
if (target.verdict) return target;
|
|
77
|
+
const { text } = target;
|
|
78
|
+
if (extractRegionBy(text, STORY_REGION.headingRe).count === SINGLE_HEADING) {
|
|
79
|
+
return { verdict: VERDICTS.present };
|
|
80
|
+
}
|
|
81
|
+
const result = planInsert({ text, spans: template.spans, cap: frontmatterMaxLines(text) });
|
|
82
|
+
if (result.refusal) return reportUndecidable(result.refusal);
|
|
83
|
+
return { verdict: VERDICTS.absent };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const buildInsertPreview = (root) => COMMAND_PREFIX + INSERT_PATH + CWD_FLAG + root;
|
|
87
|
+
|
|
88
|
+
export const PROFILE_GAPS = Object.freeze([
|
|
89
|
+
Object.freeze({ id: GAP_ID, detect: detectStorySessions, apply: buildInsertPreview }),
|
|
90
|
+
]);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
6
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
7
|
+
import { OUTCOME_LINES as LENS_OUTCOME_LINES } from './lens-region.mjs';
|
|
8
|
+
import {
|
|
9
|
+
RULES_REGIONS, readTemplateSpan, planInsert, frontmatterMaxLines, decodeUtf8Strict,
|
|
10
|
+
} from './rules-regions.mjs';
|
|
11
|
+
|
|
12
|
+
const LF = String.fromCharCode(10);
|
|
13
|
+
const BACKSLASH = String.fromCharCode(92);
|
|
14
|
+
const BACKTICK = String.fromCharCode(96);
|
|
15
|
+
const JSON_RADIX = 16;
|
|
16
|
+
const JSON_ESCAPE_WIDTH = 4;
|
|
17
|
+
const JSON_ESCAPE_PAD = '0';
|
|
18
|
+
const FIRST_CODE_POINT = 0;
|
|
19
|
+
const ARGV_OFFSET = 2;
|
|
20
|
+
const UTF8 = 'utf8';
|
|
21
|
+
const ABSENT_CODE = 'ENOENT';
|
|
22
|
+
const EXIT_SUCCESS = 0;
|
|
23
|
+
const EXIT_REFUSAL = 1;
|
|
24
|
+
const EXIT_USAGE = 2;
|
|
25
|
+
const FLAG_PREFIX = '--';
|
|
26
|
+
const CWD_FLAG = '--cwd';
|
|
27
|
+
const APPLY_FLAG = '--apply';
|
|
28
|
+
const FLAGS = Object.freeze({ [CWD_FLAG]: 'cwd', [APPLY_FLAG]: 'apply' });
|
|
29
|
+
const TEMPLATE_CANON = 'template';
|
|
30
|
+
const REGIONS = RULES_REGIONS.filter(({ canon }) => canon === TEMPLATE_CANON);
|
|
31
|
+
const TARGET_RELATIVE = 'docs/ai/agent_rules.md';
|
|
32
|
+
const TEMPLATE_PATH = resolve(
|
|
33
|
+
dirname(fileURLToPath(import.meta.url)), '..', 'references', 'templates', 'agent_rules.md',
|
|
34
|
+
);
|
|
35
|
+
const REINSTALL = 'npx @sabaiway/agent-workflow-kit@latest init';
|
|
36
|
+
const USAGE = 'usage: rules-insert.mjs --cwd <project> [--apply]';
|
|
37
|
+
const NON_REGULAR_CAUSE = 'Target is not a regular file';
|
|
38
|
+
const REFUSALS = Object.freeze({
|
|
39
|
+
fileAbsent: 'file-absent',
|
|
40
|
+
fileSymlink: 'file-symlink',
|
|
41
|
+
fileUnreadable: 'file-unreadable',
|
|
42
|
+
templateDefect: 'template',
|
|
43
|
+
headingTwice: 'heading-twice',
|
|
44
|
+
anchorAbsent: 'anchor-absent',
|
|
45
|
+
capRefused: 'cap',
|
|
46
|
+
writeFailed: 'write-failed',
|
|
47
|
+
});
|
|
48
|
+
const COMPOSERS = Object.fromEntries(Object.entries(REFUSALS).map(([key, name]) => [name, key]));
|
|
49
|
+
const DIAGNOSTIC_REFUSALS = new Set([
|
|
50
|
+
REFUSALS.templateDefect, REFUSALS.fileUnreadable, REFUSALS.writeFailed,
|
|
51
|
+
]);
|
|
52
|
+
const UNICODE_UNSAFE_RANGE = BACKSLASH + 'u007f-' + BACKSLASH + 'u009f'
|
|
53
|
+
+ BACKSLASH + 'u2028' + BACKSLASH + 'u2029';
|
|
54
|
+
const LINE_UNSAFE = new RegExp('[' + UNICODE_UNSAFE_RANGE + ']', 'g');
|
|
55
|
+
const HUMAN_UNSAFE = new RegExp(
|
|
56
|
+
'[' + BACKSLASH + 'u0000-' + BACKSLASH + 'u001f' + UNICODE_UNSAFE_RANGE + ']+', 'g',
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const escapeUnsafe = (character) => BACKSLASH + 'u'
|
|
60
|
+
+ character.codePointAt(FIRST_CODE_POINT).toString(JSON_RADIX).padStart(JSON_ESCAPE_WIDTH, JSON_ESCAPE_PAD);
|
|
61
|
+
const renderOneLine = (value) => String(value).replace(HUMAN_UNSAFE, ' ');
|
|
62
|
+
const readCause = (error) => String(error?.message ?? error);
|
|
63
|
+
const makeResult = (code, stdout = '', stderr = '') => ({ code, stdout, stderr });
|
|
64
|
+
|
|
65
|
+
export const OUTCOME_LINES = Object.freeze({
|
|
66
|
+
regionPresent: (id) => `${renderOneLine(id)}: present`,
|
|
67
|
+
regionPlanned: (id) => `${renderOneLine(id)}: planned`,
|
|
68
|
+
regionInserted: (id) => `${renderOneLine(id)}: inserted`,
|
|
69
|
+
fileAbsent: (target) => `${renderOneLine(target)} is absent; the file is seeded at bootstrap.`,
|
|
70
|
+
fileSymlink: (target) => `${renderOneLine(target)} is a symbolic link; no changes were made.`,
|
|
71
|
+
fileUnreadable: (target) => `${renderOneLine(target)} cannot be read as a regular file; no changes were made.`,
|
|
72
|
+
templateDefect: () => 'The bundled rules cannot be used; reinstall the kit with '
|
|
73
|
+
+ BACKTICK + REINSTALL + BACKTICK + '.',
|
|
74
|
+
headingTwice: (label, target) => `The heading "${renderOneLine(label)}" occurs more than once in ${renderOneLine(target)}; no changes were made.`,
|
|
75
|
+
anchorAbsent: (target) => `${renderOneLine(target)} has no numbered section in part 2 to insert after; no changes were made.`,
|
|
76
|
+
capRefused: (target, count, cap) => `${renderOneLine(target)} would contain ${renderOneLine(count)} lines, exceeding its limit of ${renderOneLine(cap)}; trim the file and try again.`,
|
|
77
|
+
writeFailed: (target) => `${renderOneLine(target)} could not be replaced; its contents were not changed.`,
|
|
78
|
+
refusalLine: (name) => `[rules-insert] refusal=${renderOneLine(name)}`,
|
|
79
|
+
errorDetail: (raw) => `[rules-insert] error=${JSON.stringify(String(raw)).replace(LINE_UNSAFE, escapeUnsafe)}`,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const refuse = (name, args = [], cause) => {
|
|
83
|
+
const lines = [OUTCOME_LINES[COMPOSERS[name]](...args), OUTCOME_LINES.refusalLine(name)];
|
|
84
|
+
if (DIAGNOSTIC_REFUSALS.has(name)) lines.push(OUTCOME_LINES.errorDetail(cause));
|
|
85
|
+
return makeResult(EXIT_REFUSAL, '', lines.join(LF));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const parseArgs = (argv) => {
|
|
89
|
+
const parsed = argv.reduce((state, argument) => {
|
|
90
|
+
if (state.pending) {
|
|
91
|
+
if (!argument || argument.startsWith(FLAG_PREFIX)) {
|
|
92
|
+
throw new Error(`${state.pending} needs a value`);
|
|
93
|
+
}
|
|
94
|
+
return { options: { ...state.options, [FLAGS[state.pending]]: argument }, pending: null };
|
|
95
|
+
}
|
|
96
|
+
if (!Object.hasOwn(FLAGS, argument)) throw new Error(`unknown argument ${argument}`);
|
|
97
|
+
const key = FLAGS[argument];
|
|
98
|
+
if (Object.hasOwn(state.options, key)) throw new Error(`${argument} repeated`);
|
|
99
|
+
if (argument === APPLY_FLAG) return { options: { ...state.options, apply: true }, pending: null };
|
|
100
|
+
return { options: state.options, pending: argument };
|
|
101
|
+
}, { options: {}, pending: null });
|
|
102
|
+
if (parsed.pending) throw new Error(`${parsed.pending} needs a value`);
|
|
103
|
+
if (!parsed.options.cwd) throw new Error(`${CWD_FLAG} is required`);
|
|
104
|
+
return parsed.options;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const readTemplate = (deps) => {
|
|
108
|
+
try {
|
|
109
|
+
const read = deps.readFileSync ?? readFileSync;
|
|
110
|
+
const text = read(deps.templatePath ?? TEMPLATE_PATH, UTF8);
|
|
111
|
+
const spans = Object.fromEntries(REGIONS.map(({ id, headingRe }) => [
|
|
112
|
+
id, readTemplateSpan(text, headingRe),
|
|
113
|
+
]));
|
|
114
|
+
const defective = REGIONS.find(({ id }) => spans[id].defect);
|
|
115
|
+
if (defective) {
|
|
116
|
+
return { error: refuse(REFUSALS.templateDefect, [], `${defective.id}: ${spans[defective.id].defect}`) };
|
|
117
|
+
}
|
|
118
|
+
return { spans };
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return { error: refuse(REFUSALS.templateDefect, [], readCause(error)) };
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const readDocument = (target, deps) => {
|
|
125
|
+
const probe = deps.lstatSync ?? lstatSync;
|
|
126
|
+
const read = deps.readFileSync ?? readFileSync;
|
|
127
|
+
const status = (() => {
|
|
128
|
+
try {
|
|
129
|
+
return { stat: probe(target) };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const name = error?.code === ABSENT_CODE ? REFUSALS.fileAbsent : REFUSALS.fileUnreadable;
|
|
132
|
+
return { error: refuse(name, [target], readCause(error)) };
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
if (status.error) return status;
|
|
136
|
+
if (status.stat.isSymbolicLink()) return { error: refuse(REFUSALS.fileSymlink, [target]) };
|
|
137
|
+
if (!status.stat.isFile()) {
|
|
138
|
+
return { error: refuse(REFUSALS.fileUnreadable, [target], NON_REGULAR_CAUSE) };
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return { text: decodeUtf8Strict(read(target)) };
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return { error: refuse(REFUSALS.fileUnreadable, [target], readCause(error)) };
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const refusePlan = (plan, target) => {
|
|
148
|
+
if (plan.refusal === REFUSALS.headingTwice) {
|
|
149
|
+
const region = REGIONS.find(({ id }) => id === plan.region);
|
|
150
|
+
return refuse(plan.refusal, [region.label, target]);
|
|
151
|
+
}
|
|
152
|
+
if (plan.refusal === REFUSALS.capRefused) return refuse(plan.refusal, [target, plan.count, plan.cap]);
|
|
153
|
+
return refuse(plan.refusal, [target]);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const runInsert = (options, deps) => {
|
|
157
|
+
const template = readTemplate(deps);
|
|
158
|
+
if (template.error) return template.error;
|
|
159
|
+
const root = resolve(options.cwd);
|
|
160
|
+
const target = resolve(root, TARGET_RELATIVE);
|
|
161
|
+
const document = readDocument(target, deps);
|
|
162
|
+
if (document.error) return document.error;
|
|
163
|
+
const cap = frontmatterMaxLines(document.text);
|
|
164
|
+
const plan = planInsert({ text: document.text, spans: template.spans, cap });
|
|
165
|
+
if (plan.refusal) return refusePlan(plan, target);
|
|
166
|
+
const planned = new Set(plan.planned.map(({ id }) => id));
|
|
167
|
+
const hasPlanned = Boolean(planned.size);
|
|
168
|
+
if (options.apply && hasPlanned) {
|
|
169
|
+
try {
|
|
170
|
+
writeContainedFileAtomic(root, target, plan.text, {
|
|
171
|
+
writeFile: deps.writeFile,
|
|
172
|
+
rename: deps.rename,
|
|
173
|
+
rm: deps.rm,
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return refuse(REFUSALS.writeFailed, [target], readCause(error));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const composePlanned = options.apply ? OUTCOME_LINES.regionInserted : OUTCOME_LINES.regionPlanned;
|
|
180
|
+
const stdout = REGIONS.map(({ id }) => (
|
|
181
|
+
planned.has(id) ? composePlanned(id) : OUTCOME_LINES.regionPresent(id)
|
|
182
|
+
)).join(LF);
|
|
183
|
+
const stderr = hasPlanned && cap === null ? LENS_OUTCOME_LINES.capSkipNote() : '';
|
|
184
|
+
return makeResult(EXIT_SUCCESS, stdout, stderr);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const main = (argv, deps = {}) => {
|
|
188
|
+
const parsed = (() => {
|
|
189
|
+
try {
|
|
190
|
+
return { options: parseArgs(argv) };
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return { error: makeResult(EXIT_USAGE, '', OUTCOME_LINES.errorDetail(`${USAGE}; ${readCause(error)}`)) };
|
|
193
|
+
}
|
|
194
|
+
})();
|
|
195
|
+
return parsed.error ?? runInsert(parsed.options, deps);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (isDirectRun(import.meta.url)) {
|
|
199
|
+
const result = await main(process.argv.slice(ARGV_OFFSET));
|
|
200
|
+
if (result.stdout) console.log(result.stdout);
|
|
201
|
+
if (result.stderr) console.error(result.stderr);
|
|
202
|
+
process.exitCode = result.code;
|
|
203
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const LF = String.fromCharCode(10);
|
|
2
|
+
const CR = String.fromCharCode(13);
|
|
3
|
+
const CRLF = CR + LF;
|
|
4
|
+
const BACKSLASH = String.fromCharCode(92);
|
|
5
|
+
const FRONTMATTER_BOUNDARY = '---';
|
|
6
|
+
const STRUCTURAL_HEADING_RE = /^#{2,3} /;
|
|
7
|
+
const NUMBERED_HEADING_RE = /^### 2[.]([0-9]+)[.]/;
|
|
8
|
+
const NUMBERED_HEADING_PREFIX = '### 2.';
|
|
9
|
+
const MAX_LINES_RE = new RegExp('^maxLines:' + BACKSLASH + 's*([0-9]+)' + BACKSLASH + 's*$');
|
|
10
|
+
const TEMPLATE_CANON = 'template';
|
|
11
|
+
const HEADING_COUNT_DEFECT = 'heading-count';
|
|
12
|
+
const HEADING_TWICE_REFUSAL = 'heading-twice';
|
|
13
|
+
const ANCHOR_ABSENT_REFUSAL = 'anchor-absent';
|
|
14
|
+
const CAP_REFUSAL = 'cap';
|
|
15
|
+
const SINGLE_HEADING = 1;
|
|
16
|
+
const NOT_FOUND = -1;
|
|
17
|
+
const UTF8_LABEL = 'utf-8';
|
|
18
|
+
const UTF8_ENCODING = 'utf8';
|
|
19
|
+
const ROUND_TRIP_MESSAGE = 'the bytes read do not survive a UTF-8 round trip';
|
|
20
|
+
const BOM_CHAR = String.fromCharCode(65279);
|
|
21
|
+
|
|
22
|
+
// Append the OUTGOING canon here in the same release that changes the template
|
|
23
|
+
// §2.5 region — the fragment-or-prior reconcile depends on it.
|
|
24
|
+
const COMMS_PRIOR_PRE_AD054 = `### 2.x. Communication (user-facing messages)
|
|
25
|
+
Apply this as part of §2 before any user-facing summary:
|
|
26
|
+
- **Deliver the artifact IN the message** — paste the prompt / diff / version / command inline; never "see §X / open the file / run it and you'll see" as a *substitute* for showing what was asked.
|
|
27
|
+
- **Lead with the result**, then the details; show exactly what was asked — no deflection, no "almost done" when the ask was the finished thing.
|
|
28
|
+
- **No condescension, no filler.** Own a miss plainly and fix it in the same message.
|
|
29
|
+
- **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.`;
|
|
30
|
+
const COMMS_PRIOR_AD054 = `${COMMS_PRIOR_PRE_AD054}
|
|
31
|
+
- **Live host/session facts are tool-composed only.** Any claim about the current host or session state (prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts) must trace to **live tool output** from **this session**; a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection.`;
|
|
32
|
+
const COMMS_PRIOR_PLAIN_LANGUAGE = `### 2.x. Communication (user-facing messages)
|
|
33
|
+
Apply this as part of §2 before any user-facing summary:
|
|
34
|
+
- **Plain language.** User-facing narration is short, clear, plain words of the dialogue language; when the dialogue language is not English, transliterated English jargon is banned — an English term survives only as the NAME of a thing (a flag / command / file / test), glossed in plain words when helpful; plain English stays plain for English-dialogue users.
|
|
35
|
+
- **Deliver the artifact IN the message** — paste the prompt / diff / version / command inline; never "see §X / open the file / run it and you'll see" as a *substitute* for showing what was asked.
|
|
36
|
+
- **Lead with the result**, then the details; show exactly what was asked — no deflection, no "almost done" when the ask was the finished thing.
|
|
37
|
+
- **No condescension, no filler.** Own a miss plainly and fix it in the same message.
|
|
38
|
+
- **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.
|
|
39
|
+
- **Live host/session facts are tool-composed only.** Any claim about the current host or session state (prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts) must trace to **live tool output** from **this session**; a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection.`;
|
|
40
|
+
const COMMS_PRIOR_STATE_BLOCK = `${COMMS_PRIOR_PLAIN_LANGUAGE}
|
|
41
|
+
- **The closing state block answers three DIFFERENT questions.** Close a user-facing message with three labelled slots — *now* · *what I need from you* · *what's next*. The slot LABELS stay ENGLISH — an English label is what lets a state-block checker FIND the block and its slots at all; everything written INTO a slot is in the project's dialogue language; when that language is not English, the checker's English phrase sets do not judge those values. **Now** = the state at this instant: what is RUNNING, or what the work is stopped on. It is **never a report of finished work** — what you completed goes in the message BODY, above the block. **From you** = the real unblocker, named; a turn that is ENDING always has one. **Next** = what follows. A *now* slot that opens with what was completed buries the one fact the reader opened the message for, and the three slots collapse into one restatement.`;
|
|
42
|
+
const COMMS_PRIORS = [COMMS_PRIOR_PRE_AD054, COMMS_PRIOR_AD054, COMMS_PRIOR_PLAIN_LANGUAGE, COMMS_PRIOR_STATE_BLOCK];
|
|
43
|
+
|
|
44
|
+
export const RULES_REGIONS = Object.freeze([
|
|
45
|
+
Object.freeze({
|
|
46
|
+
id: 'communication',
|
|
47
|
+
headingRe: /^### 2[.]([0-9]+)[.] Communication [(]user-facing messages[)]/,
|
|
48
|
+
label: '### 2.x. Communication (user-facing messages)',
|
|
49
|
+
canon: TEMPLATE_CANON,
|
|
50
|
+
priors: COMMS_PRIORS,
|
|
51
|
+
}),
|
|
52
|
+
Object.freeze({
|
|
53
|
+
id: 'story-sessions',
|
|
54
|
+
headingRe: /^### 2[.]([0-9]+)[.] Story sessions/,
|
|
55
|
+
label: '### 2.x. Story sessions',
|
|
56
|
+
canon: TEMPLATE_CANON,
|
|
57
|
+
priors: [],
|
|
58
|
+
}),
|
|
59
|
+
Object.freeze({
|
|
60
|
+
id: 'lens',
|
|
61
|
+
headingRe: /^### 2[.]([0-9]+)[.] Planning, review & process-fidelity/,
|
|
62
|
+
label: '### 2.x. Planning, review & process-fidelity invariants',
|
|
63
|
+
canon: 'engine',
|
|
64
|
+
priors: [],
|
|
65
|
+
}),
|
|
66
|
+
]);
|
|
67
|
+
const TEMPLATE_REGIONS = RULES_REGIONS.filter(({ canon }) => canon === TEMPLATE_CANON);
|
|
68
|
+
|
|
69
|
+
const stripCr = (line) => (line.endsWith(CR) ? line.slice(0, -1) : line);
|
|
70
|
+
const isBoundary = (line) => line === FRONTMATTER_BOUNDARY || STRUCTURAL_HEADING_RE.test(line);
|
|
71
|
+
const lineCount = (text) => text.split(LF).length - (text.endsWith(LF) ? 1 : 0);
|
|
72
|
+
const findHeadings = (lines, headingRe) => lines.flatMap((line, index) => {
|
|
73
|
+
const match = stripCr(line).match(headingRe);
|
|
74
|
+
return match ? [{ index, number: match[1] }] : [];
|
|
75
|
+
});
|
|
76
|
+
const regionEnd = (lines, start) => {
|
|
77
|
+
const afterHeading = start + 1;
|
|
78
|
+
const boundary = lines.slice(afterHeading).findIndex((line) => isBoundary(stripCr(line)));
|
|
79
|
+
return boundary === NOT_FOUND ? lines.length : afterHeading + boundary;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const extractRegionBy = (text, headingRe) => {
|
|
83
|
+
const lines = String(text).split(LF);
|
|
84
|
+
const headings = findHeadings(lines, headingRe);
|
|
85
|
+
const count = headings.length;
|
|
86
|
+
if (count === 0) return { found: false, count: 0 };
|
|
87
|
+
const { index: start, number } = headings[0];
|
|
88
|
+
const end = regionEnd(lines, start);
|
|
89
|
+
const regionLines = lines.slice(start, end).map(stripCr);
|
|
90
|
+
const bodyEnd = regionLines.findLastIndex((line) => line.trim() !== '') + 1;
|
|
91
|
+
const body = regionLines.slice(0, bodyEnd).join(LF);
|
|
92
|
+
return { found: true, count, start, end, number, body };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const readTemplateSpan = (text, headingRe) => {
|
|
96
|
+
const region = extractRegionBy(text, headingRe);
|
|
97
|
+
if (region.count !== SINGLE_HEADING) return { defect: HEADING_COUNT_DEFECT };
|
|
98
|
+
const span = String(text).split(LF).slice(region.start, region.end).map(stripCr).join(LF);
|
|
99
|
+
return { span, number: region.number };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// A reader that will WRITE the whole document back must not decode leniently: readFileSync(path,
|
|
103
|
+
// 'utf8') turns an invalid byte into U+FFFD, and the rewrite would carry that substitution into
|
|
104
|
+
// bytes nobody asked to change. The guarantee the callers need is not "the decoder is strict" but
|
|
105
|
+
// "re-encoding gives back the bytes we read" — so it is asserted, not reasoned about: any decoder
|
|
106
|
+
// asymmetry (a substitution, a stripped byte-order mark) fails here rather than in the written file.
|
|
107
|
+
// Bytes in, text out, or one throw the caller answers by its own name.
|
|
108
|
+
export const decodeUtf8Strict = (bytes) => {
|
|
109
|
+
const text = new TextDecoder(UTF8_LABEL, { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
110
|
+
if (!Buffer.from(text, UTF8_ENCODING).equals(bytes)) throw new Error(ROUND_TRIP_MESSAGE);
|
|
111
|
+
return text;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// The decoded text KEEPS a leading byte-order mark, because the writer must give the bytes back
|
|
115
|
+
// exactly; a structural reader must therefore look past it, or a file that declares maxLines reads
|
|
116
|
+
// as one that declares none. Only judgement skips it — never the text any offset is computed over.
|
|
117
|
+
export const frontmatterMaxLines = (text) => {
|
|
118
|
+
const lines = String(text).split(LF).map(stripCr);
|
|
119
|
+
const opening = lines[0]?.startsWith(BOM_CHAR) ? lines[0].slice(BOM_CHAR.length) : lines[0];
|
|
120
|
+
if (opening !== FRONTMATTER_BOUNDARY) return null;
|
|
121
|
+
for (const line of lines.slice(1)) {
|
|
122
|
+
if (line === FRONTMATTER_BOUNDARY) return null;
|
|
123
|
+
const match = line.match(MAX_LINES_RE);
|
|
124
|
+
if (match) return Number(match[1]);
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const exceedsCap = (text, cap) => {
|
|
130
|
+
const count = lineCount(text);
|
|
131
|
+
const skipped = cap === null;
|
|
132
|
+
return { over: !skipped && count > cap, count, skipped };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const planInsert = ({ text, spans, cap }) => {
|
|
136
|
+
const regions = TEMPLATE_REGIONS.map(({ id, headingRe }) => ({
|
|
137
|
+
id,
|
|
138
|
+
region: extractRegionBy(text, headingRe),
|
|
139
|
+
}));
|
|
140
|
+
const doubled = regions.find(({ region }) => region.count > SINGLE_HEADING);
|
|
141
|
+
if (doubled) return { refusal: HEADING_TWICE_REFUSAL, region: doubled.id };
|
|
142
|
+
const missing = regions.filter(({ region }) => !region.found);
|
|
143
|
+
if (missing.length === 0) return { planned: [], text };
|
|
144
|
+
const lines = text.split(LF);
|
|
145
|
+
const headings = findHeadings(lines, NUMBERED_HEADING_RE);
|
|
146
|
+
if (headings.length === 0) return { refusal: ANCHOR_ABSENT_REFUSAL };
|
|
147
|
+
const insertAt = regionEnd(lines, headings.at(-1).index);
|
|
148
|
+
const highestNumber = Math.max(...headings.map(({ number }) => Number(number)));
|
|
149
|
+
const planned = missing.map(({ id }, index) => ({
|
|
150
|
+
id,
|
|
151
|
+
number: String(highestNumber + index + 1),
|
|
152
|
+
insertAt,
|
|
153
|
+
}));
|
|
154
|
+
const newline = text.includes(CRLF) ? CRLF : LF;
|
|
155
|
+
const offset = insertAt === lines.length
|
|
156
|
+
? text.length
|
|
157
|
+
: lines.slice(0, insertAt).reduce((length, line) => length + line.length + LF.length, 0);
|
|
158
|
+
const prefix = offset === text.length && !text.endsWith(LF) ? newline : '';
|
|
159
|
+
const inserted = planned.map(({ id, number }) => {
|
|
160
|
+
const bound = spans[id].span.replace(NUMBERED_HEADING_RE, NUMBERED_HEADING_PREFIX + number + '.');
|
|
161
|
+
return bound.split(LF).map(stripCr).join(newline) + newline;
|
|
162
|
+
}).join('');
|
|
163
|
+
const resultText = text.slice(0, offset) + prefix + inserted + text.slice(offset);
|
|
164
|
+
const decision = exceedsCap(resultText, cap);
|
|
165
|
+
if (decision.over) return { refusal: CAP_REFUSAL, count: decision.count, cap };
|
|
166
|
+
return { planned, text: resultText };
|
|
167
|
+
};
|