@sabaiway/agent-workflow-kit 14.1.0 → 14.3.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 +68 -0
- package/README.md +3 -3
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/agents.md +1 -1
- package/references/modes/dispatch.md +3 -3
- package/references/modes/grounding.md +1 -1
- package/references/modes/procedures.md +6 -4
- package/references/modes/recipes.md +2 -2
- package/references/modes/recommendations.md +1 -1
- package/references/modes/set-recipe.md +4 -2
- package/references/modes/status.md +1 -1
- package/references/templates/orchestration.json +1 -1
- package/tools/carriers.mjs +6 -1
- package/tools/checkpoint-restore.mjs +62 -5
- package/tools/checkpoint.mjs +16 -7
- package/tools/dispatch-baseline.mjs +49 -21
- package/tools/dispatch-store.mjs +2 -2
- package/tools/dispatch.mjs +37 -17
- package/tools/exec-producer.mjs +4 -4
- package/tools/inject-methodology.mjs +6 -4
- package/tools/orchestration-readme.mjs +10 -2
- package/tools/procedures.mjs +11 -7
- package/tools/task-brief.mjs +20 -2
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
// spec:checkpoint
|
|
1
|
+
// restoreCheckpoint, restoreTaskThread — spec:checkpoint, spec:task-thread; docs/ai/specs/kit/checkpoint/restore.md
|
|
2
2
|
import { unlinkSync, rmdirSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
-
import { resolveTreeObject, snapshotScope } from './dispatch-baseline.mjs';
|
|
4
|
+
import { resolveTreeObject, snapshotScope, computeBasePayload } from './dispatch-baseline.mjs';
|
|
5
5
|
import { readDelegationLedger } from './dispatch-store-read.mjs';
|
|
6
|
+
import { isThreadTerminalRecord } from './dispatch-record.mjs';
|
|
7
|
+
import { readsTask, readsInsideEpoch, epochClaims } from './task-thread.mjs';
|
|
8
|
+
import { hasPathOverlap } from './claim-relation.mjs';
|
|
6
9
|
import { CHECKPOINT_REF_PREFIX, withRepository, readGit, readGitBytes, readCheckpointRefs,
|
|
7
10
|
withTemporaryIndex, readScopeStat, writeSnapshot, findOpenCheckpointThread } from './checkpoint-core.mjs';
|
|
8
11
|
|
|
@@ -20,8 +23,14 @@ const SEQUENCE_REF_PATTERN = /^[^/]+\/(?:0|[1-9][0-9]*)$/;
|
|
|
20
23
|
const ALLOWED_MODES = ['100644', '100755', '120000'];
|
|
21
24
|
const LEDGER_ABSENT = 'absent';
|
|
22
25
|
const LEDGER_OK = 'ok';
|
|
26
|
+
const HEAD_ERROR = 'error';
|
|
27
|
+
const DISPATCH_KIND = 'dispatch';
|
|
28
|
+
const FOLD_KIND = 'fold';
|
|
29
|
+
const CHECKPOINT_KIND = 'checkpoint';
|
|
30
|
+
const FOLDED = 'folded';
|
|
23
31
|
const NONEMPTY_CODES = ['ENOTEMPTY', 'EEXIST'];
|
|
24
|
-
const NAMES = { scope: 'target-scope', ledger: 'ledger', open: 'open-thread', conflict: 'type-conflict'
|
|
32
|
+
const NAMES = { scope: 'target-scope', ledger: 'ledger', open: 'open-thread', conflict: 'type-conflict',
|
|
33
|
+
task: 'task-target', folded: 'task-folded' };
|
|
25
34
|
const ENCODING_ERROR = 'target tree paths are not lossless UTF-8';
|
|
26
35
|
const ENTRY_ERROR = 'unreadable target tree entry';
|
|
27
36
|
const UNREFERENCED_ERROR = 'no checkpoint sequence carries target';
|
|
@@ -31,10 +40,11 @@ const GIT_ARGS = {
|
|
|
31
40
|
entries: ['ls-tree', '-r', '-z'],
|
|
32
41
|
read: ['read-tree'],
|
|
33
42
|
checkout: ['checkout-index', '-a', '-f'],
|
|
43
|
+
checkoutPaths: ['checkout-index', '-f', '--'],
|
|
34
44
|
};
|
|
35
45
|
|
|
36
46
|
const refuse = (reason) => ({ ok: false, code: REFUSE, reason });
|
|
37
|
-
const readTargetEntries = (context, target) => {
|
|
47
|
+
const readTargetEntries = (context, target, files = null) => {
|
|
38
48
|
const bytes = readGitBytes(context, [...GIT_ARGS.entries, target]);
|
|
39
49
|
const text = bytes.toString(UTF8);
|
|
40
50
|
if (!Buffer.from(text, UTF8).equals(bytes)) throw new Error(`${NAMES.scope}: ${ENCODING_ERROR}`);
|
|
@@ -42,11 +52,12 @@ const readTargetEntries = (context, target) => {
|
|
|
42
52
|
const parsed = ENTRY_PATTERN.exec(entry);
|
|
43
53
|
if (parsed === null) throw new Error(`${NAMES.scope}: ${ENTRY_ERROR}`);
|
|
44
54
|
const [, mode, path] = parsed;
|
|
55
|
+
if (files !== null && !files.includes(path)) return null;
|
|
45
56
|
if (!ALLOWED_MODES.includes(mode) || path.split(SLASH).some((segment) => FORBIDDEN_SEGMENTS.includes(segment))) {
|
|
46
57
|
throw new Error(`${NAMES.scope}: ${path}`);
|
|
47
58
|
}
|
|
48
59
|
return path;
|
|
49
|
-
});
|
|
60
|
+
}).filter((path) => path !== null);
|
|
50
61
|
};
|
|
51
62
|
const readHeldSet = (context, target) => {
|
|
52
63
|
const refs = readCheckpointRefs(context).filter(({ ref }) => SEQUENCE_REF_PATTERN.test(ref.slice(CHECKPOINT_REF_PREFIX.length)));
|
|
@@ -124,3 +135,49 @@ export const restoreCheckpoint = (cwd, oid, io = {}) => withRepository(cwd, io,
|
|
|
124
135
|
return snapshot.oid === target ? { ok: true, target, proof: snapshot.oid }
|
|
125
136
|
: refuse(`${MISMATCH}: ${target} ${snapshot.oid}`);
|
|
126
137
|
});
|
|
138
|
+
|
|
139
|
+
export const restoreTaskThread = (cwd, oid, nonce, io = {}) => withRepository(cwd, io, (context) => {
|
|
140
|
+
const tree = resolveTreeObject(context.top, oid, context.io);
|
|
141
|
+
if (!tree.ok) return refuse(tree.reason);
|
|
142
|
+
const target = readGit(context, [...GIT_ARGS.resolve, oid]);
|
|
143
|
+
const ledger = readDelegationLedger(context.cwd, context.env);
|
|
144
|
+
if (ledger.state !== LEDGER_OK) return refuse(`${NAMES.ledger}: ${ledger.reason ?? ledger.state}`);
|
|
145
|
+
if (ledger.head.state === HEAD_ERROR) return refuse(`${NAMES.ledger}: ${ledger.head.reason}`);
|
|
146
|
+
const { records, head } = ledger;
|
|
147
|
+
const index = records.findIndex((record) => record.kind === DISPATCH_KIND && record.nonce === nonce);
|
|
148
|
+
if (index === LAST) return refuse(`${NAMES.task}: ${nonce}`);
|
|
149
|
+
const dispatch = records[index];
|
|
150
|
+
const task = readsTask(dispatch);
|
|
151
|
+
if (task === null) return refuse(`${NAMES.task}: ${nonce}`);
|
|
152
|
+
const later = records.slice(index + NEXT).filter((record) => record.nonce === nonce);
|
|
153
|
+
if (!later.some(isThreadTerminalRecord)) return refuse(`${NAMES.open}: ${nonce}`);
|
|
154
|
+
if (later.some((record) => record.kind === FOLD_KIND)) return refuse(`${NAMES.folded}: ${nonce}`);
|
|
155
|
+
if (dispatch.baseline.treeOid !== target) return refuse(`${NAMES.task}: ${nonce}: ${target}`);
|
|
156
|
+
const scope = snapshotScope(context.top, { baseTreeOid: target }, context.io);
|
|
157
|
+
if (!scope.ok) return refuse(scope.reason);
|
|
158
|
+
const paths = scope.paths.filter((path) => task.files.includes(path));
|
|
159
|
+
const domain = new Set(paths);
|
|
160
|
+
for (const claim of epochClaims(records, head, isThreadTerminalRecord)) {
|
|
161
|
+
if (claim.nonce === nonce || !claim.files.some((file) => paths.some((path) => hasPathOverlap(file, path)))) continue;
|
|
162
|
+
return refuse(`${claim.state === FOLDED ? NAMES.folded : NAMES.open}: ${claim.nonce}`);
|
|
163
|
+
}
|
|
164
|
+
const untasked = records.filter((record) => record.kind !== DISPATCH_KIND || readsTask(record) === null);
|
|
165
|
+
const folded = untasked.find((record, position) => record.kind === DISPATCH_KIND && readsInsideEpoch(record, head)
|
|
166
|
+
&& untasked.slice(position + NEXT).some((successor) => successor.nonce === record.nonce && successor.kind === FOLD_KIND));
|
|
167
|
+
if (folded !== undefined) return refuse(`${NAMES.folded}: ${folded.nonce}`);
|
|
168
|
+
const held = readHeldSet(context, target);
|
|
169
|
+
const open = findOpenCheckpointThread(untasked, held);
|
|
170
|
+
if (open !== undefined) return refuse(`${NAMES.open}: ${open.nonce}`);
|
|
171
|
+
const entries = readTargetEntries(context, target, task.files);
|
|
172
|
+
const outside = entries.find((path) => !domain.has(path));
|
|
173
|
+
if (outside !== undefined) return refuse(`${NAMES.scope}: ${outside}`);
|
|
174
|
+
const types = checkTypes(context, entries);
|
|
175
|
+
if (!types.ok) return types;
|
|
176
|
+
withTemporaryIndex(context.env, (env) => {
|
|
177
|
+
readGit(context, [...GIT_ARGS.read, target], env);
|
|
178
|
+
if (entries.length > START) readGit(context, [...GIT_ARGS.checkoutPaths, ...entries], env);
|
|
179
|
+
});
|
|
180
|
+
removeExtras(context, paths, new Set(entries));
|
|
181
|
+
const proof = computeBasePayload(context.top, { kind: CHECKPOINT_KIND, treeOid: target }, context.io, task.files);
|
|
182
|
+
return proof !== null && proof.length === START ? { ok: true, target, nonce } : refuse(MISMATCH);
|
|
183
|
+
});
|
package/tools/checkpoint.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { PLANS_REL, isScratchPlanName } from './plan-files.mjs';
|
|
|
7
7
|
import { isDirectRun } from './direct-run.mjs';
|
|
8
8
|
import { CHECKPOINT_REF_PREFIX, refuse, withRepository, readGit, readCheckpointRefs, writeSnapshot,
|
|
9
9
|
findOpenCheckpointThread } from './checkpoint-core.mjs';
|
|
10
|
-
import { restoreCheckpoint } from './checkpoint-restore.mjs';
|
|
10
|
+
import { restoreCheckpoint, restoreTaskThread } from './checkpoint-restore.mjs';
|
|
11
11
|
|
|
12
12
|
export { CHECKPOINT_REF_PREFIX } from './checkpoint-core.mjs';
|
|
13
13
|
const ACCEPT = 0;
|
|
@@ -30,11 +30,15 @@ const LEDGER_OK = 'ok';
|
|
|
30
30
|
const CLEAN = 'CLEAN';
|
|
31
31
|
const DIRTY = 'DIRTY';
|
|
32
32
|
const PROOF = 'proof';
|
|
33
|
+
const TASK_KIND = 'task';
|
|
34
|
+
const TASK_PROOF = 'clean';
|
|
33
35
|
const PLAN_FLAG = '--plan';
|
|
36
|
+
const NONCE_FLAG = '--nonce';
|
|
34
37
|
const PLAN_ARG_COUNT = 3;
|
|
35
38
|
const OID_ARG_COUNT = 2;
|
|
39
|
+
const TASK_ARG_COUNT = 4;
|
|
36
40
|
const ARGV_OFFSET = 2;
|
|
37
|
-
const USAGE_TEXT = 'usage: checkpoint mint|newest|prune --plan <plan> | verify|restore <oid>';
|
|
41
|
+
const USAGE_TEXT = 'usage: checkpoint mint|newest|prune --plan <plan> | verify <oid> | restore <oid> [--nonce <n>]';
|
|
38
42
|
const PLAN_ERROR = 'expected a readable regular plan directly under docs/plans';
|
|
39
43
|
const SEQUENCE_ERROR = 'expected contiguous numbered refs on trees';
|
|
40
44
|
const GIT_ARGS = {
|
|
@@ -122,17 +126,22 @@ const PLAN_OPERATIONS = { [VERBS.mint]: mintCheckpoint, [VERBS.newest]: newestCh
|
|
|
122
126
|
const formatRef = ({ stem, n, oid }) => `${CHECKPOINT_KIND} ${stem}/${n} ${oid}${NEWLINE}`;
|
|
123
127
|
const formatRefusal = ({ code, reason }) => ({ code, stdout: EMPTY, stderr: `${reason}${NEWLINE}` });
|
|
124
128
|
export const main = (argv = process.argv.slice(ARGV_OFFSET), deps = {}) => {
|
|
125
|
-
const [verb, operand, planPath] = argv;
|
|
129
|
+
const [verb, operand, planPath, nonce] = argv;
|
|
126
130
|
const planVerb = Object.hasOwn(PLAN_OPERATIONS, verb);
|
|
127
131
|
const oidVerb = verb === VERBS.verify || verb === VERBS.restore;
|
|
132
|
+
const taskRestore = verb === VERBS.restore && argv.length === TASK_ARG_COUNT && operand
|
|
133
|
+
&& planPath === NONCE_FLAG && typeof nonce === STRING && nonce !== EMPTY;
|
|
128
134
|
if (!(planVerb && argv.length === PLAN_ARG_COUNT && operand === PLAN_FLAG && planPath)
|
|
129
|
-
&& !(oidVerb && argv.length === OID_ARG_COUNT && operand)) return formatRefusal(refuse(USAGE_TEXT, USAGE));
|
|
135
|
+
&& !(oidVerb && argv.length === OID_ARG_COUNT && operand) && !taskRestore) return formatRefusal(refuse(USAGE_TEXT, USAGE));
|
|
130
136
|
const cwd = deps.cwd ?? process.cwd();
|
|
131
137
|
const operation = verb === VERBS.restore ? restoreCheckpoint : verifyCheckpoint;
|
|
132
|
-
const result =
|
|
138
|
+
const result = taskRestore ? restoreTaskThread(cwd, operand, nonce, deps)
|
|
139
|
+
: planVerb ? PLAN_OPERATIONS[verb](cwd, planPath, deps) : operation(cwd, operand, deps);
|
|
133
140
|
if (!result.ok) return formatRefusal(result);
|
|
134
|
-
if (verb === VERBS.restore)
|
|
135
|
-
|
|
141
|
+
if (verb === VERBS.restore) {
|
|
142
|
+
const proof = taskRestore ? `${TASK_KIND} ${nonce} ${PROOF} ${TASK_PROOF}` : `${PROOF} ${result.proof}`;
|
|
143
|
+
return { code: ACCEPT, stdout: `${CHECKPOINT_KIND} ${VERBS.restore} ${result.target} ${proof}${NEWLINE}`, stderr: EMPTY };
|
|
144
|
+
}
|
|
136
145
|
if (verb === VERBS.verify) return { code: result.clean ? ACCEPT : REFUSE,
|
|
137
146
|
stdout: `${result.clean ? CLEAN : DIRTY} ${result.target} ${result.oid}${NEWLINE}`, stderr: EMPTY };
|
|
138
147
|
return { code: ACCEPT, stdout: verb === VERBS.prune ? EMPTY : formatRef(result), stderr: EMPTY };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dispatch-baseline.mjs — snapshotScope, computeBasePayload, computeBaseFingerprint,
|
|
1
|
+
// dispatch-baseline.mjs — snapshotScope, computeBasePayload, computeBaseFingerprint, taskFilesScopeRefusal,
|
|
2
2
|
// isCleanAgainstBase, runBaseDiff and resolveTreeObject; spec:dispatch-baseline.
|
|
3
3
|
// Writes only its own temporary indexes and the blob objects stored by forced adds.
|
|
4
4
|
// Head measurements delegate unchanged to core-evidence without a temporary index.
|
|
@@ -20,6 +20,8 @@ const PAYLOAD_DIFF_ARGS = ['--no-ext-diff', '--no-textconv', '--ignore-submodule
|
|
|
20
20
|
const SCOPE_EXCLUSIONS = ['/docs/plans/', '/.claude/settings.json', '/.claude/settings.local.json', '/.mcp.json'];
|
|
21
21
|
const MISSING_PATH_CODES = ['ENOENT', 'ENOTDIR'];
|
|
22
22
|
const ABSENT_HEAD_STATUS = 1;
|
|
23
|
+
const CHECK_IGNORE_CLEAR_STATUS = 1;
|
|
24
|
+
const EXCLUDE_SOURCES = ['.gitignore', '.gitattributes'];
|
|
23
25
|
const splitPaths = (bytes) => {
|
|
24
26
|
const text = bytes.toString('utf8');
|
|
25
27
|
if (!Buffer.from(text).equals(bytes)) throw new Error('git path names are not lossless UTF-8');
|
|
@@ -62,12 +64,12 @@ const readStat = (path, lstat) => {
|
|
|
62
64
|
throw error;
|
|
63
65
|
}
|
|
64
66
|
};
|
|
65
|
-
const readTreePaths = (run, top, treeOid, env) => splitPaths(readGit(run, ['ls-tree', '-r', '--name-only', '-z', treeOid], top, env));
|
|
66
|
-
const readHeadPaths = (run, top, env) => {
|
|
67
|
+
const readTreePaths = (run, top, treeOid, env, directories = false) => splitPaths(readGit(run, ['ls-tree', '-r', ...(directories ? ['-d', '-t'] : []), '--name-only', '-z', treeOid], top, env));
|
|
68
|
+
const readHeadPaths = (run, top, env, directories = false) => {
|
|
67
69
|
const head = run(['rev-parse', '--verify', '--quiet', 'HEAD'], top, undefined, env);
|
|
68
70
|
if (head && !head.error && !head.signal && head.status === ABSENT_HEAD_STATUS) return [];
|
|
69
71
|
if (!head || head.error || head.signal || head.status !== 0) throw new Error('cannot resolve HEAD');
|
|
70
|
-
return readTreePaths(run, top, 'HEAD', env);
|
|
72
|
+
return readTreePaths(run, top, 'HEAD', env, directories);
|
|
71
73
|
};
|
|
72
74
|
const excludePatterns = (top, paths) => [
|
|
73
75
|
...SCOPE_EXCLUSIONS,
|
|
@@ -99,8 +101,7 @@ const collectScope = (top, baseTreeOid, run, lstat, env) => withTemporaryIndex(e
|
|
|
99
101
|
|
|
100
102
|
export const snapshotScope = (cwd, { baseTreeOid = null } = {}, io = {}) => {
|
|
101
103
|
try {
|
|
102
|
-
const env = io.env ?? process.env;
|
|
103
|
-
const run = makeRunner(io);
|
|
104
|
+
const env = io.env ?? process.env, run = makeRunner(io);
|
|
104
105
|
const top = locateTop(cwd, run, env);
|
|
105
106
|
return { ok: true, paths: collectScope(top, baseTreeOid, run, io.lstat ?? lstatSync, env) };
|
|
106
107
|
} catch (error) {
|
|
@@ -110,8 +111,7 @@ export const snapshotScope = (cwd, { baseTreeOid = null } = {}, io = {}) => {
|
|
|
110
111
|
|
|
111
112
|
export const resolveTreeObject = (cwd, oid, io = {}) => {
|
|
112
113
|
try {
|
|
113
|
-
const env = io.env ?? process.env;
|
|
114
|
-
const run = makeRunner(io);
|
|
114
|
+
const env = io.env ?? process.env, run = makeRunner(io);
|
|
115
115
|
const top = locateTop(cwd, run, env);
|
|
116
116
|
const type = readGit(run, ['cat-file', '-t', oid], top, env).toString('utf8').trim();
|
|
117
117
|
return type === 'tree' ? { ok: true } : { ok: false, reason: `${oid} names a ${type}, not a tree` };
|
|
@@ -120,19 +120,47 @@ export const resolveTreeObject = (cwd, oid, io = {}) => {
|
|
|
120
120
|
}
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
-
export const
|
|
123
|
+
export const taskFilesScopeRefusal = (cwd, base, paths, io = {}) => {
|
|
124
|
+
if (paths.length === 0) return null;
|
|
125
|
+
const current = { path: paths[0] };
|
|
126
|
+
try {
|
|
127
|
+
const env = io.env ?? process.env, run = makeRunner(io), lstat = io.lstat ?? lstatSync;
|
|
128
|
+
const top = locateTop(cwd, run, env);
|
|
129
|
+
return withTemporaryIndex(env, (indexEnv) => {
|
|
130
|
+
readGit(run, ['read-tree', '--empty'], top, indexEnv);
|
|
131
|
+
const treePaths = readHeadPaths(run, top, indexEnv).concat(base.kind === 'checkpoint' ? readTreePaths(run, top, base.treeOid, indexEnv) : []);
|
|
132
|
+
const directories = readHeadPaths(run, top, indexEnv, true).concat(base.kind === 'checkpoint' ? readTreePaths(run, top, base.treeOid, indexEnv, true) : []);
|
|
133
|
+
const matchesPattern = (path, pattern) => { const probe = patternToProbe(pattern); return isDirPattern(pattern) ? path === probe.slice(0, -1) || path.startsWith(probe) : path === probe; };
|
|
134
|
+
const exclusions = excludePatterns(top, paths);
|
|
135
|
+
for (const path of paths) {
|
|
136
|
+
current.path = path;
|
|
137
|
+
if (EXCLUDE_SOURCES.includes(basename(path))) return `${path}: exclude-source`;
|
|
138
|
+
if (path.split('/').includes('node_modules') || exclusions.some((pattern) => matchesPattern(path, pattern))) return `${path}: excluded`;
|
|
139
|
+
if (readStat(join(top, path), lstat)?.isDirectory() || directories.includes(path)) return `${path}: directory`;
|
|
140
|
+
const prefixes = path.split('/').slice(0, -1).map((segment, index, parts) => parts.slice(0, index + 1).join('/'));
|
|
141
|
+
if (prefixes.some((prefix) => { const stat = readStat(join(top, prefix), lstat); return (stat !== null && !stat.isDirectory()) || treePaths.includes(prefix); })) return `${path}: beneath-non-directory`;
|
|
142
|
+
if (treePaths.includes(path) || KIT_OWN_PATHS.some((pattern) => matchesPattern(path, pattern))) continue;
|
|
143
|
+
const { GIT_LITERAL_PATHSPECS: _literalPathspecs, ...ignoreEnv } = indexEnv; const ignored = run(['check-ignore', '--no-index', '-q', '--', path], top, undefined, ignoreEnv);
|
|
144
|
+
if (!ignored || ignored.error || ignored.signal || ![0, CHECK_IGNORE_CLEAR_STATUS].includes(ignored.status)) throw new Error(`git check-ignore failed: ${ignored?.error?.message ?? String(ignored?.stderr ?? '').trim()}`);
|
|
145
|
+
if (ignored.status === 0) return `${path}: ignored`;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
});
|
|
149
|
+
} catch (error) { return `${current.path}: ${error.message}`; }
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const runBaseDiff = (cwd, base, args, io = {}, paths = null) => {
|
|
124
153
|
if (base.kind !== 'checkpoint') return null;
|
|
125
154
|
try {
|
|
126
|
-
const env = io.env ?? process.env;
|
|
127
|
-
const run = makeRunner(io);
|
|
155
|
+
const env = io.env ?? process.env, run = makeRunner(io);
|
|
128
156
|
const top = locateTop(cwd, run, env);
|
|
129
157
|
if (!resolveTreeObject(top, base.treeOid, io).ok) return null;
|
|
130
158
|
const scope = snapshotScope(top, { baseTreeOid: base.treeOid }, io);
|
|
131
159
|
if (!scope.ok) return null;
|
|
132
160
|
const basePaths = readTreePaths(run, top, base.treeOid, env);
|
|
133
|
-
const
|
|
134
|
-
const removed = [];
|
|
135
|
-
for (const path of
|
|
161
|
+
const scopedPaths = paths === null ? scope.paths : scope.paths.filter((path) => paths.includes(path));
|
|
162
|
+
const present = [], removed = [];
|
|
163
|
+
for (const path of scopedPaths) {
|
|
136
164
|
const stat = readStat(join(top, path), io.lstat ?? lstatSync);
|
|
137
165
|
const baseHoldsPath = basePaths.includes(path);
|
|
138
166
|
const neverCommittable = stat !== null && isNeverCommittableStat(stat);
|
|
@@ -141,7 +169,7 @@ export const runBaseDiff = (cwd, base, args, io = {}) => {
|
|
|
141
169
|
if (materialized) present.push(path);
|
|
142
170
|
if (!materialized && !neverCommittable && (stat === null || baseHoldsPath)) removed.push(path);
|
|
143
171
|
}
|
|
144
|
-
const excludedBasePaths = basePaths.filter((path) => !
|
|
172
|
+
const excludedBasePaths = basePaths.filter((path) => !scopedPaths.includes(path));
|
|
145
173
|
const conflict = present.find((path) => excludedBasePaths.some((excluded) =>
|
|
146
174
|
path.startsWith(`${excluded}/`) || excluded.startsWith(`${path}/`)));
|
|
147
175
|
if (conflict !== undefined) throw new Error(`live path ${conflict} conflicts with an excluded base path`);
|
|
@@ -156,17 +184,17 @@ export const runBaseDiff = (cwd, base, args, io = {}) => {
|
|
|
156
184
|
}
|
|
157
185
|
};
|
|
158
186
|
|
|
159
|
-
export const computeBasePayload = (cwd, base, io = {}) => base.kind === 'head'
|
|
187
|
+
export const computeBasePayload = (cwd, base, io = {}, paths = null) => base.kind === 'head'
|
|
160
188
|
? computeFingerprintPayload(cwd, io)
|
|
161
|
-
: runBaseDiff(cwd, base, PAYLOAD_DIFF_ARGS, io);
|
|
189
|
+
: runBaseDiff(cwd, base, PAYLOAD_DIFF_ARGS, io, paths);
|
|
162
190
|
|
|
163
|
-
export const computeBaseFingerprint = (cwd, base, io) => {
|
|
164
|
-
const payload = computeBasePayload(cwd, base, io);
|
|
191
|
+
export const computeBaseFingerprint = (cwd, base, io, paths = null) => {
|
|
192
|
+
const payload = computeBasePayload(cwd, base, io, paths);
|
|
165
193
|
return payload === null ? null : createHash(HASH_ALGORITHM).update(payload).digest('hex');
|
|
166
194
|
};
|
|
167
195
|
|
|
168
|
-
export const isCleanAgainstBase = (cwd, base, io) => {
|
|
196
|
+
export const isCleanAgainstBase = (cwd, base, io, paths = null) => {
|
|
169
197
|
if (base.kind === 'head') return isTreeClean(cwd, io);
|
|
170
|
-
const payload = computeBasePayload(cwd, base, io);
|
|
198
|
+
const payload = computeBasePayload(cwd, base, io, paths);
|
|
171
199
|
return payload === null ? null : payload.length === 0;
|
|
172
200
|
};
|
package/tools/dispatch-store.mjs
CHANGED
|
@@ -33,8 +33,8 @@ export const resolveDelegationLockPath = (storePath) => `${storePath}${DELEGATIO
|
|
|
33
33
|
|
|
34
34
|
export const UNCOMMITTED_STATE_FINGERPRINT = 'the uncommitted-state fingerprint';
|
|
35
35
|
|
|
36
|
-
export const uncommittedStateFingerprint = (cwd = process.cwd(), fsx, base = HEAD_BASELINE) => {
|
|
37
|
-
const fingerprint = base.kind === 'head' ? computeTreeFingerprint(cwd, fsx) : computeBaseFingerprint(cwd, base, fsx);
|
|
36
|
+
export const uncommittedStateFingerprint = (cwd = process.cwd(), fsx, base = HEAD_BASELINE, paths = null) => {
|
|
37
|
+
const fingerprint = base.kind === 'head' ? computeTreeFingerprint(cwd, fsx) : computeBaseFingerprint(cwd, base, fsx, paths);
|
|
38
38
|
if (fingerprint == null) {
|
|
39
39
|
throw stop(`cannot compute ${UNCOMMITTED_STATE_FINGERPRINT} — not inside a git work tree (or a git probe failed); a record never carries a null tree digest (fail closed)`);
|
|
40
40
|
}
|
package/tools/dispatch.mjs
CHANGED
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
// `gate-output` component is emitted in v1. The metric counts the returned change set only.
|
|
54
54
|
// • a receipt is FORGEABLE, exactly like every record in this family. What the absorb door defends
|
|
55
55
|
// against is a buggy or interrupted producer, not a hostile one.
|
|
56
|
-
// • D10 stands as a bar, not a mechanism: at most ONE in-tree exec dispatch
|
|
57
|
-
// nothing here refuses a second one
|
|
56
|
+
// • D10 stands as a bar, not a mechanism, for untasked threads: at most ONE untasked in-tree exec dispatch
|
|
57
|
+
// runs at a time, and nothing here refuses a second one; for task threads it is a mechanism: files-overlap and mixed-open refuse conflicting opens at append time, and return and fold refuse out-of-files.
|
|
58
58
|
// • `await` observes ARRIVAL and nothing else: an expiry never authorizes the next writer, and the
|
|
59
59
|
// verb releases no slot it never held. Whether the run may be ABSORBED stays `return`'s question. A parallel-write story is not this plan's.
|
|
60
60
|
//
|
|
@@ -74,7 +74,7 @@ import {
|
|
|
74
74
|
DELEGATION_SCHEMA_VERSION, STEP_CLASSES, OBSERVATION_PROVENANCE, RETURN_OUTCOMES,
|
|
75
75
|
SESSION_ID_NULLABLE_OUTCOMES, checkDispatchContractForm, checkDispatchMintConsistency,
|
|
76
76
|
contractDigest, canonicalDelegationDigest, computeNumerator, evaluateMetricEligibility,
|
|
77
|
-
HEAD_BASELINE, readsBaseline, isTreeOid,
|
|
77
|
+
HEAD_BASELINE, readsBaseline, isTreeOid, isThreadTerminalRecord,
|
|
78
78
|
} from './dispatch-record.mjs';
|
|
79
79
|
import {
|
|
80
80
|
appendDelegationRecord, readDelegationStore, resolveDelegationStorePath, delegationThreadState,
|
|
@@ -89,7 +89,7 @@ import {
|
|
|
89
89
|
enumerateReturnedObjects, computeReturnedDiff, assembleIntegrationBundle,
|
|
90
90
|
} from './exec-producer.mjs';
|
|
91
91
|
import { gitBuf } from './core-evidence.mjs';
|
|
92
|
-
import { isCleanAgainstBase, resolveTreeObject } from './dispatch-baseline.mjs';
|
|
92
|
+
import { isCleanAgainstBase, resolveTreeObject, taskFilesScopeRefusal, runBaseDiff } from './dispatch-baseline.mjs';
|
|
93
93
|
import { readDelegationLedger } from './dispatch-store-read.mjs';
|
|
94
94
|
import { judgeLedger, threadVerdict, HELD_RECEIPT_BACKEND } from './held-session.mjs';
|
|
95
95
|
import { degradeRecordSet, selectHeldSessionDegrades } from './review-state.mjs';
|
|
@@ -100,13 +100,13 @@ import {
|
|
|
100
100
|
} from './observation-builder.mjs';
|
|
101
101
|
import { handoffReturn, HANDOFF_SLUG_RE } from './worktree-handoff-return.mjs';
|
|
102
102
|
import { parseBrief } from './task-brief.mjs';
|
|
103
|
-
import { byteOrder, validateTask } from './task-thread.mjs';
|
|
103
|
+
import { byteOrder, validateTask, readsTask, claimedPaths } from './task-thread.mjs';
|
|
104
104
|
|
|
105
105
|
const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
|
|
106
106
|
|
|
107
107
|
// The ONE contract sentence, doc-parity-bound into references/modes/dispatch.md: the FORM-only limit
|
|
108
108
|
// and the aggregator's refusals are what a reader must not be able to mis-learn from the mode doc.
|
|
109
|
-
export const DISPATCH_CONTRACT = 'the contract check is FORM-only — fields present, grammars respected, never boundedness, design-decidedness or acceptance adequacy — and `aggregate` REFUSES instead of computing acceptance for a wave with no pre-registration record, over an OPEN thread in scope, over a PRE-DISPATCH degrade that opens no thread, or across several waves with no `--wave`; the writer verbs add NO second legality door — the store\'s preflight is the only one, and its refusals travel verbatim — while `open` copies every mint-time field from the contract header and refuses a deadline below the wrapper cap plus the kill grace, `return` absorbs only a TERMINAL exec receipt whose backend, nonce and independently computed contractDigest match the dispatch it answers, and `fold` binds the CURRENT tree to the folded return\'s postTreeDigest, so a tree that moved between the two never folds, and `await` is satisfied ONLY by the TERMINAL exec receipt of its own dispatch\'s {backend, nonce} — never by a review receipt, a ledger line or a finding manifest — while an expiry names a supervision question and releases NO writer slot; `open` records the thread\'s BASE (`head` by default, `checkpoint` under `--checkpoint <oid>`), `return` and `fold` measure against the base the dispatch recorded, never a flag, and the `fold` VERB itself refuses a checkpoint `code` thread the held-session judge names SUBSTITUTED in that thread\'s own verdict — a precondition like its hidden-path and content-blind guards, the store\'s preflight staying the only door on the record';
|
|
109
|
+
export const DISPATCH_CONTRACT = 'the contract check is FORM-only — fields present, grammars respected, never boundedness, design-decidedness or acceptance adequacy — and `aggregate` REFUSES instead of computing acceptance for a wave with no pre-registration record, over an OPEN thread in scope, over a PRE-DISPATCH degrade that opens no thread, or across several waves with no `--wave`; the writer verbs add NO second legality door — the store\'s preflight is the only one, and its refusals travel verbatim — while `open` copies every mint-time field from the contract header and refuses a deadline below the wrapper cap plus the kill grace, `return` absorbs only a TERMINAL exec receipt whose backend, nonce and independently computed contractDigest match the dispatch it answers, and `fold` binds the CURRENT tree to the folded return\'s postTreeDigest, so a tree that moved between the two never folds, and `await` is satisfied ONLY by the TERMINAL exec receipt of its own dispatch\'s {backend, nonce} — never by a review receipt, a ledger line or a finding manifest — while an expiry names a supervision question and releases NO writer slot; `open` records the thread\'s BASE (`head` by default, `checkpoint` under `--checkpoint <oid>`), `return` and `fold` measure against the base the dispatch recorded, never a flag, and the `fold` VERB itself refuses a checkpoint `code` thread the held-session judge names SUBSTITUTED in that thread\'s own verdict — a precondition like its hidden-path and content-blind guards, the store\'s preflight staying the only door on the record; `open --task` records the task, a task thread is measured over its Files and `open`, `return` and `fold` refuse `files-overlap`, `mixed-open` and `out-of-files`';
|
|
110
110
|
|
|
111
111
|
// ── the flag surface (the CLI tests pin it against the D3 key sets) ────────────────────────────────
|
|
112
112
|
// flag → the record field it decides. The fields NOT on a flag are DERIVED and listed beside them,
|
|
@@ -557,8 +557,10 @@ const runOpen = ({ baseCwd, env, argv, now }) => {
|
|
|
557
557
|
task = { brief, files: [...new Set(parsed.files.map(({ path }) => path))].sort(byteOrder) };
|
|
558
558
|
const checked = validateTask(task, base.kind);
|
|
559
559
|
if (!checked.ok) return refusal('open', checked.reason);
|
|
560
|
+
const scope = taskFilesScopeRefusal(cwd, base, task.files);
|
|
561
|
+
if (scope !== null) return refusal('open', `task-files-scope: ${scope}; nothing was written`);
|
|
560
562
|
}
|
|
561
|
-
const baselineClean = isCleanAgainstBase(cwd, base);
|
|
563
|
+
const baselineClean = isCleanAgainstBase(cwd, base, undefined, task?.files ?? null);
|
|
562
564
|
if (baselineClean === null) {
|
|
563
565
|
return refusal('open', 'the working state could not be probed, so the baseline is undecidable — a dispatch never records a guessed baseline, and outside a git work tree there is no tree to fingerprint either (fail closed); nothing was written');
|
|
564
566
|
}
|
|
@@ -577,7 +579,7 @@ const runOpen = ({ baseCwd, env, argv, now }) => {
|
|
|
577
579
|
// delegate's account at return time. `open` is where that lie is cheapest to catch.
|
|
578
580
|
const honest = hiddenFromPlainDiff(top);
|
|
579
581
|
if (!honest.ok) return refusal('open', `${honest.reason}; nothing was written`);
|
|
580
|
-
const preTreeDigest = uncommittedStateFingerprint(cwd, undefined, base);
|
|
582
|
+
const preTreeDigest = uncommittedStateFingerprint(cwd, undefined, base, task?.files ?? null);
|
|
581
583
|
const record = {
|
|
582
584
|
schema: DELEGATION_SCHEMA_VERSION,
|
|
583
585
|
kind: 'dispatch',
|
|
@@ -668,7 +670,7 @@ export const pollExecArrival = ({ dir, backend, nonce, io = {} }) => {
|
|
|
668
670
|
return { state: 'satisfied', reason: `the TERMINAL exec receipt landed (${path}) — outcome ${receipt.outcome} · exit ${receipt.exitStatus} · session ${receipt.sessionId ?? 'none'}` };
|
|
669
671
|
};
|
|
670
672
|
|
|
671
|
-
const NO_SLOT_RELEASED = 'NO writer slot was released: a wait that ended without an answer never authorizes the next dispatch
|
|
673
|
+
const NO_SLOT_RELEASED = 'NO writer slot was released: a wait that ended without an answer never authorizes the next dispatch';
|
|
672
674
|
|
|
673
675
|
// The bound arithmetic is BigInt, and it HAS to be. The frozen record vocabulary admits any positive
|
|
674
676
|
// SAFE INTEGER `deadlineS` (dispatch-record.mjs), so `deadlineS * 1000` leaves the exactly
|
|
@@ -1056,6 +1058,18 @@ export const treeDriftRefusal = (openingDigest, closingDigest) => (openingDigest
|
|
|
1056
1058
|
? null
|
|
1057
1059
|
: `the tree moved WHILE the return was being computed (${openingDigest.slice(0, 12)}… → ${closingDigest.slice(0, 12)}…) — the enumeration and the diff would then describe two different change sets, and the record would bind a postTreeDigest neither of them saw; nothing was written, so leave the tree alone and return again`);
|
|
1058
1060
|
|
|
1061
|
+
const getOutOfFilesRefusal = (cwd, base, env) => {
|
|
1062
|
+
const changed = runBaseDiff(cwd, base, ['--name-only', '--no-renames', '-z'], {}, null);
|
|
1063
|
+
if (changed === null) return 'out-of-files: the snapshot domain is undecidable (fail closed); nothing was written';
|
|
1064
|
+
const ledger = readDelegationLedger(cwd, env);
|
|
1065
|
+
if (ledger.state !== 'ok') return `out-of-files: ${ledger.reason ?? `delegation ledger is ${ledger.state}`} (fail closed); nothing was written`;
|
|
1066
|
+
const { records, head } = ledger;
|
|
1067
|
+
if (head.state === 'error') return `out-of-files: ${head.reason} (fail closed); nothing was written`;
|
|
1068
|
+
const claimed = new Set(claimedPaths(records, base.treeOid, head, isThreadTerminalRecord));
|
|
1069
|
+
const unclaimed = changed.toString('utf8').split('\0').filter((path) => path !== '' && !claimed.has(path)).sort(byteOrder);
|
|
1070
|
+
return unclaimed.length === 0 ? null : `out-of-files: ${unclaimed.join(', ')}; nothing was written`;
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1059
1073
|
const runReturn = ({ baseCwd, env, argv, now }) => {
|
|
1060
1074
|
const { values, operands, cwd } = scan(argv, { ...RETURN_FLAG_FIELDS, ...RETURN_INPUT_FLAGS }, baseCwd, { booleans: RETURN_BOOLEAN_FLAGS });
|
|
1061
1075
|
refuseOperands('return', operands);
|
|
@@ -1156,20 +1170,23 @@ const runReturn = ({ baseCwd, env, argv, now }) => {
|
|
|
1156
1170
|
// A tree that moved between them would hand the numerator one change set and the denominator
|
|
1157
1171
|
// another, and the record would bind a postTreeDigest neither of them describes.
|
|
1158
1172
|
const base = readsBaseline(dispatch);
|
|
1159
|
-
const
|
|
1160
|
-
const
|
|
1173
|
+
const paths = readsTask(dispatch)?.files ?? null;
|
|
1174
|
+
const openingDigest = uncommittedStateFingerprint(cwd, undefined, base, paths);
|
|
1175
|
+
const enumerated = enumerateReturnedObjects(cwd, { base, paths });
|
|
1161
1176
|
if (!enumerated.ok) return refusal('return', enumerated.reason);
|
|
1162
1177
|
const opaque = contentBlindRefusal(enumerated.entries);
|
|
1163
1178
|
if (opaque !== null) return refusal('return', `${opaque}; nothing was written`);
|
|
1164
1179
|
const lostTarget = symlinkTargetRefusal(top, enumerated.entries);
|
|
1165
1180
|
if (lostTarget !== null) return refusal('return', `${lostTarget}; nothing was written`);
|
|
1166
|
-
const produced = computeReturnedDiff(cwd, { base });
|
|
1181
|
+
const produced = computeReturnedDiff(cwd, { base, paths });
|
|
1167
1182
|
if (!produced.ok) return refusal('return', produced.reason);
|
|
1168
1183
|
// The payload IS the diff and its digest IS the uncommitted-state fingerprint — one computation,
|
|
1169
1184
|
// so diffDigest and postTreeDigest are equal by construction rather than by coincidence.
|
|
1170
1185
|
const postTreeDigest = sha256(produced.diff);
|
|
1171
1186
|
const drift = treeDriftRefusal(openingDigest, postTreeDigest);
|
|
1172
1187
|
if (drift !== null) return refusal('return', drift);
|
|
1188
|
+
const outOfFiles = paths === null ? null : getOutOfFilesRefusal(cwd, base, env);
|
|
1189
|
+
if (outOfFiles !== null) return refusal('return', outOfFiles);
|
|
1173
1190
|
// The fail-closed arm of a producer contradiction (one objectId claimed at two sizes). The
|
|
1174
1191
|
// enumeration is this kit's own and emits one entry per object, so nothing here can reach it —
|
|
1175
1192
|
// the vocabulary stays the authority on what a component is, and its refusal is surfaced whole.
|
|
@@ -1290,7 +1307,8 @@ const runFold = ({ baseCwd, env, argv, now }) => {
|
|
|
1290
1307
|
// return and the fold without moving the digest this fold is about to bind. The enumeration runs
|
|
1291
1308
|
// here for that one question — the fold computes no metric of its own.
|
|
1292
1309
|
const base = readsBaseline(state.dispatch);
|
|
1293
|
-
const
|
|
1310
|
+
const paths = readsTask(state.dispatch)?.files ?? null;
|
|
1311
|
+
const enumerated = enumerateReturnedObjects(cwd, { base, paths });
|
|
1294
1312
|
if (!enumerated.ok) return refusal('fold', enumerated.reason);
|
|
1295
1313
|
const opaque = contentBlindRefusal(enumerated.entries);
|
|
1296
1314
|
if (opaque !== null) return refusal('fold', `${opaque}; nothing was written`);
|
|
@@ -1327,7 +1345,9 @@ const runFold = ({ baseCwd, env, argv, now }) => {
|
|
|
1327
1345
|
// fold is honest there — identical payload bytes, identical content — so the rule is "the fold
|
|
1328
1346
|
// precedes staging", never "staging refuses the fold by construction". A checkpoint measurement
|
|
1329
1347
|
// never reads the real index, so staging moves nothing there; the fold still precedes `git add`.
|
|
1330
|
-
const treeDigestAtFold = uncommittedStateFingerprint(cwd, undefined, base);
|
|
1348
|
+
const treeDigestAtFold = uncommittedStateFingerprint(cwd, undefined, base, paths);
|
|
1349
|
+
const outOfFiles = paths === null ? null : getOutOfFilesRefusal(cwd, base, env);
|
|
1350
|
+
if (outOfFiles !== null) return refusal('fold', outOfFiles);
|
|
1331
1351
|
const record = {
|
|
1332
1352
|
schema: DELEGATION_SCHEMA_VERSION,
|
|
1333
1353
|
kind: 'fold',
|
|
@@ -1672,7 +1692,7 @@ refusal from the store verbatim.
|
|
|
1672
1692
|
--checkpoint <oid> records a resolvable TREE object of exactly 40 or 64 lowercase hex characters
|
|
1673
1693
|
(a commit, blob, tag or unknown id refuses by name); the base rides the record and return/fold read
|
|
1674
1694
|
it there, while a checkpoint CLEAN line names the base.
|
|
1675
|
-
--task records the brief's path and its byte-ordered Files, requires --checkpoint (task-base), refuses a brief the parser refuses by the parser's name, and the store refuses files-overlap and mixed-open at append time.
|
|
1695
|
+
--task records the brief's path and its byte-ordered Files, requires --checkpoint (task-base), refuses a brief the parser refuses by the parser's name, refuses task-files-scope for a Files path the snapshot domain can never hold, and the store refuses files-overlap and mixed-open at append time; a task thread is measured over its Files.
|
|
1676
1696
|
|
|
1677
1697
|
THE LEDGER a tree-binding verb uses is the CANONICAL one, exactly: <git common dir>/
|
|
1678
1698
|
agent-workflow-delegation.jsonl. A store inside the work tree is measured as part of the change set
|
|
@@ -1800,8 +1820,8 @@ diff: a mode change moves it and no untracked marker line exists. Under a head b
|
|
|
1800
1820
|
arrives as a deletion plus a creation, over-counting a plain rename; under a checkpoint base git's own
|
|
1801
1821
|
rename detection applies inside the one base-relative layer and a detected rename is counted once.
|
|
1802
1822
|
A receipt is forgeable exactly like every record here;
|
|
1803
|
-
and D10 stands as a BAR, not a mechanism — at most one in-tree exec dispatch at a time,
|
|
1804
|
-
refuses a second
|
|
1823
|
+
and D10 stands as a BAR, not a mechanism, for untasked threads — at most one untasked in-tree exec dispatch at a time,
|
|
1824
|
+
and nothing refuses a second; for task threads it is a mechanism: files-overlap and mixed-open refuse conflicting opens at append time, and return and fold refuse out-of-files.
|
|
1805
1825
|
|
|
1806
1826
|
Never commits, never runs a subscription CLI, spawns nothing but git READS — with TWO stated exceptions: handoff-return,
|
|
1807
1827
|
which attests MAIN's index with git write-tree: that may write a tree OBJECT into the odb and moves
|
package/tools/exec-producer.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// the DENOMINATOR) while HEAD→worktree shows nothing at all, so the object would vanish from the
|
|
15
15
|
// NUMERATOR and the two halves of one ratio would describe different change sets. The diff bytes are
|
|
16
16
|
// computeFingerprintPayload's own bytes, imported rather than rebuilt. Under a checkpoint base the
|
|
17
|
-
// walk is one layer against the base tree, through the leaf's temporary index.
|
|
17
|
+
// walk is one layer against the base tree, through the leaf's temporary index; io.paths absent or null = the whole domain.
|
|
18
18
|
//
|
|
19
19
|
// ONE OBJECT, ONE ENTRY. Under a head base an object touched in both layers is a single entry, and its pre-image is
|
|
20
20
|
// always the HEAD blob — never the index blob: the numerator answers what the delegate could have
|
|
@@ -359,11 +359,11 @@ const entryForUntracked = (top, rel, lstat) => {
|
|
|
359
359
|
};
|
|
360
360
|
|
|
361
361
|
const enumerateCheckpointObjects = (top, base, io) => {
|
|
362
|
-
const raw = runBaseDiff(top, base, BASE_RAW_ARGS, io);
|
|
362
|
+
const raw = runBaseDiff(top, base, BASE_RAW_ARGS, io, io.paths ?? null);
|
|
363
363
|
if (raw === null) return refuse('git could not read the base-relative change set (fail closed)');
|
|
364
364
|
const parsed = parseRawRecords(raw, 'the base-relative change set');
|
|
365
365
|
if (!parsed.ok) return parsed;
|
|
366
|
-
const numstat = runBaseDiff(top, base, BASE_NUMSTAT_ARGS, io);
|
|
366
|
+
const numstat = runBaseDiff(top, base, BASE_NUMSTAT_ARGS, io, io.paths ?? null);
|
|
367
367
|
if (numstat === null) return refuse('git could not read the numstat binary markers (fail closed)');
|
|
368
368
|
const binary = parseNumstatMarkers(numstat, 'the base-relative binary markers');
|
|
369
369
|
if (!binary.ok) return binary;
|
|
@@ -482,7 +482,7 @@ export const computeReturnedDiff = (cwd = process.cwd(), io = {}) => {
|
|
|
482
482
|
// caller that framed a bundle around a thrown read would have no bytes and no reason either.
|
|
483
483
|
try {
|
|
484
484
|
const base = io.base ?? HEAD_BASELINE;
|
|
485
|
-
const payload = base.kind === 'checkpoint' ? computeBasePayload(cwd, base, io) : computeFingerprintPayload(cwd);
|
|
485
|
+
const payload = base.kind === 'checkpoint' ? computeBasePayload(cwd, base, io, io.paths ?? null) : computeFingerprintPayload(cwd);
|
|
486
486
|
return payload == null
|
|
487
487
|
? refuse('the canonical payload could not be computed (fail closed)')
|
|
488
488
|
: { ok: true, diff: payload };
|
|
@@ -90,6 +90,8 @@ export const KNOWN_PRIOR_METHODOLOGY_SLOT = [
|
|
|
90
90
|
'> **Workflow methodology** — plan → execute → review. Plans are ephemeral `docs/plans/*.md` (gitignored, **never committed**); every Plan ends with a mandatory **Phase: Cleanup**; series order lives in `docs/plans/queue.md`. The plan shape, its caps and lifecycle live in the project\'s **planning skill** (it overrides the generic `writing-plans`); summary in `docs/ai/agent_rules.md` §5. Named activities (plan-authoring, plan-execution) have procedures — see `/agent-workflow-kit procedures <activity>` for the steps + resolved recipe. **Communication:** user-facing messages deliver the artifact inline (paste the prompt / diff / command — never "see §X" as a substitute), lead with the result, show exactly what was asked, and never read as mockery (a large artifact: a real summary inline + a link).',
|
|
91
91
|
// engine 4.6.0 — the three-activity pointer, before feedback-triage joined the named activities.
|
|
92
92
|
'> **Workflow methodology** — plan → execute → review. Plans are ephemeral `docs/plans/*.md` (gitignored, **never committed**); every Plan ends with a mandatory **Phase: Cleanup**; series order lives in `docs/plans/queue.md`. The plan shape, its caps and lifecycle live in the project\'s **planning skill** (it overrides the generic `writing-plans`); summary in `docs/ai/agent_rules.md` §5. Named activities (plan-authoring, plan-execution, routine) have procedures — see `/agent-workflow-kit procedures <activity>` for the steps + resolved recipe. **Communication:** user-facing messages deliver the artifact inline (paste the prompt / diff / command — never "see §X" as a substitute), lead with the result, show exactly what was asked, and never read as mockery (a large artifact: a real summary inline + a link).',
|
|
93
|
+
// engine 5.7.0 — the four-activity pointer, before epic and task joined the named activities.
|
|
94
|
+
'> **Workflow methodology** — plan → execute → review. Plans are ephemeral `docs/plans/*.md` (gitignored, **never committed**); every Plan ends with a mandatory **Phase: Cleanup**; series order lives in `docs/plans/queue.md`. The plan shape, its caps and lifecycle live in the project\'s **planning skill** (it overrides the generic `writing-plans`); summary in `docs/ai/agent_rules.md` §5. Named activities (plan-authoring, plan-execution, routine, feedback-triage) have procedures — see `/agent-workflow-kit procedures <activity>` for the steps + resolved recipe. **Communication:** user-facing messages deliver the artifact inline (paste the prompt / diff / command — never "see §X" as a substitute), lead with the result, show exactly what was asked, and never read as mockery (a large artifact: a real summary inline + a link).',
|
|
93
95
|
];
|
|
94
96
|
export const KNOWN_PRIOR_ORCH_SLOT = [
|
|
95
97
|
// v1.3.0 — pre-read-at-start orchestration pointer (recipes vocabulary, no orchestration.json clause).
|
|
@@ -113,9 +115,9 @@ export const METHODOLOGY_DESCRIPTOR = {
|
|
|
113
115
|
markerName: 'methodology',
|
|
114
116
|
anchorLabel: 'methodology anchor (the "Read it before any code change." Session-Protocols line)',
|
|
115
117
|
knownPriorCanonicals: KNOWN_PRIOR_METHODOLOGY_SLOT,
|
|
116
|
-
upgradeSignature: '
|
|
118
|
+
upgradeSignature: 'feedback-triage, epic, task',
|
|
117
119
|
upgradeAdvice:
|
|
118
|
-
'the workflow-methodology pointer predates the communication-contract clause (deliver the artifact inline, lead with the result) — refresh it to the current canon, or add
|
|
120
|
+
'the workflow-methodology pointer predates the current canon — six named activities (plan-authoring, plan-execution, routine, feedback-triage, epic, task) with the communication-contract clause (deliver the artifact inline, lead with the result) — refresh it to the current canon, or add what it lacks by hand; the contract still applies.',
|
|
119
121
|
};
|
|
120
122
|
export const ORCHESTRATION_DESCRIPTOR = {
|
|
121
123
|
startMarker: ORCH_START_MARKER,
|
|
@@ -443,8 +445,8 @@ export const runCli = async (argv, deps = {}) => {
|
|
|
443
445
|
'present-filled': 'workflow-methodology pointer already present',
|
|
444
446
|
}[methResult.status];
|
|
445
447
|
// Read-only upgrade advisories for a CUSTOMIZED methodology pointer that reconcile preserved verbatim
|
|
446
|
-
// (a refreshed/filled/inserted slot already carries the current canon, so it gets none): the AD-019
|
|
447
|
-
//
|
|
448
|
+
// (a refreshed/filled/inserted slot already carries the current canon, so it gets none): the AD-019 procedures route
|
|
449
|
+
// AND the current-canon hint, keyed on the six-activity token (it names the §1.9 clause too). No mutation — notes only.
|
|
448
450
|
const notes = [];
|
|
449
451
|
if (methResult.status === 'present-filled') {
|
|
450
452
|
const p = methodologyProceduresHint(afterMeth); if (p) notes.push(p);
|
|
@@ -60,12 +60,20 @@ const FOUR_README = FOLD_README.replace(
|
|
|
60
60
|
"Four activities are configured independently, and so is each slot within them: 'plan-authoring' (slots author, fold, review), 'plan-execution' (slots execute, review), 'routine' (slots carrier, parallel) and 'feedback-triage' (slot review).",
|
|
61
61
|
);
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
const FOUR_SEED_README = FOUR_README.replace(
|
|
64
64
|
"The default below is 'solo' for every recipe and carrier slot, and 'on' for the parallel switch — no execution backend required.",
|
|
65
65
|
"Every slot seeded below is 'solo' — no execution backend required. A slot the seed leaves silent takes the computed default stated further down: 'feedback-triage.review' is reviewed as soon as a review backend is ready (solo until then); 'plan-authoring.author', 'plan-authoring.fold' and 'routine.carrier' stay 'solo', and 'routine.parallel' stays 'on'.",
|
|
66
66
|
);
|
|
67
67
|
|
|
68
|
-
export const
|
|
68
|
+
export const CANON_README = FOUR_SEED_README
|
|
69
|
+
.replace("Four activities are configured independently, and so is each slot within them: 'plan-authoring' (slots author, fold, review), 'plan-execution' (slots execute, review), 'routine' (slots carrier, parallel) and 'feedback-triage' (slot review).",
|
|
70
|
+
"Six activities are configured independently, and so is each slot within them: 'plan-authoring' (slots author, fold, review), 'plan-execution' (slots execute, review), 'routine' (slots carrier, parallel), 'feedback-triage' (slot review), 'epic' (slots author, review) and 'task' (slots author, execute).")
|
|
71
|
+
.replace("the carrier slots 'plan-authoring.author', 'plan-authoring.fold' and 'routine.carrier' accept solo | subagent.",
|
|
72
|
+
"the carrier slots 'plan-authoring.author', 'plan-authoring.fold', 'routine.carrier' and 'epic.author' accept solo | subagent, and the task slots 'task.author' and 'task.execute' take the execute values.")
|
|
73
|
+
.replace("'feedback-triage.review' is reviewed as soon as a review backend is ready (solo until then); 'plan-authoring.author', 'plan-authoring.fold' and 'routine.carrier' stay 'solo',",
|
|
74
|
+
"'feedback-triage.review' is reviewed as soon as a review backend is ready (solo until then), and so is 'epic.review'; 'plan-authoring.author', 'plan-authoring.fold', 'routine.carrier', 'epic.author', 'task.author' and 'task.execute' stay 'solo',");
|
|
75
|
+
|
|
76
|
+
export const KNOWN_PRIOR_README = Object.freeze([V1_README, V2_README, V3_README, ROSTER_README, FOLD_README, FOUR_README, FOUR_SEED_README]);
|
|
69
77
|
|
|
70
78
|
export const refreshReadme = (config) => {
|
|
71
79
|
if (config == null || typeof config !== 'object' || Array.isArray(config)) return { config, changed: false };
|
package/tools/procedures.mjs
CHANGED
|
@@ -276,6 +276,7 @@ export const REVIEW_ROUNDS_TOOL = join(dirname(fileURLToPath(import.meta.url)),
|
|
|
276
276
|
export const REPO_SEARCH_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'repo-search.mjs');
|
|
277
277
|
export const FEEDBACK_RECORD_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'feedback-record-cli.mjs');
|
|
278
278
|
const FEEDBACK_TRIAGE = 'feedback-triage';
|
|
279
|
+
const EPIC = 'epic';
|
|
279
280
|
const GROUNDING_FACTS_OUT = '/tmp/review-facts.md';
|
|
280
281
|
const MINTS_RECEIPT = Object.freeze({ mintsReceipt: true });
|
|
281
282
|
const FEEDBACK_BRIDGE_LINES = Object.freeze({
|
|
@@ -380,25 +381,28 @@ const groundingPreStepAdvice = (activity, slots, plans) => {
|
|
|
380
381
|
if (activity === FEEDBACK_TRIAGE) return [];
|
|
381
382
|
if (!slots.some((s) => (s.backends ?? []).includes('agy-review'))) return [];
|
|
382
383
|
const operand = populatedPlan(plans);
|
|
383
|
-
const planArg = operand === null ? '--plan <path>' :
|
|
384
|
+
const planArg = activity === EPIC ? '' : operand === null ? ' --plan <path>' : ` --plan ${operand}`;
|
|
384
385
|
const reviewOperand = populatedPlan(plans, MINTS_RECEIPT);
|
|
385
386
|
// plan-authoring reviews the plan FILE — a plain name in flight renders the review command populated;
|
|
386
387
|
// the renderability and receipt-carriability fallbacks are the only placeholders a known path produces.
|
|
387
388
|
const reviewForm =
|
|
388
|
-
activity ===
|
|
389
|
-
?
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
389
|
+
activity === EPIC
|
|
390
|
+
? 'agy-review plan <brief-file>'
|
|
391
|
+
: activity === 'plan-authoring'
|
|
392
|
+
? reviewOperand === null
|
|
393
|
+
? 'agy-review plan <plan-file>'
|
|
394
|
+
: `agy-review plan ${reviewOperand}`
|
|
395
|
+
: 'agy-review code';
|
|
393
396
|
// `run:`/`then:` prefixes keep these POPULATED command lines machine-distinguishable from the
|
|
394
397
|
// verbatim contract DESCRIPTORS above (the descriptor drift guard set-equals bare wrapper lines).
|
|
395
398
|
// The TOOL path stays double-quoted (the bridge tier seeds that exact byte-form); the plan operand
|
|
396
399
|
// rides shellQuoteArg — bare when safe, single-quoted otherwise.
|
|
397
400
|
const lines = [
|
|
398
401
|
'Grounding pre-step (agy is dispatched — assemble the verified facts BEFORE the review; grounding.mjs slices verbatim, judgment additions stay yours):',
|
|
399
|
-
` run: node "${GROUNDING_TOOL}" --constraints --autonomy
|
|
402
|
+
` run: node "${GROUNDING_TOOL}" --constraints --autonomy${planArg} --out ${GROUNDING_FACTS_OUT}`,
|
|
400
403
|
` then: ${reviewForm} --facts @${GROUNDING_FACTS_OUT}`,
|
|
401
404
|
];
|
|
405
|
+
if (activity === EPIC) return lines;
|
|
402
406
|
const fellBack = [operand === null ? '--plan' : null, activity === 'plan-authoring' && reviewOperand === null ? 'agy-review plan' : null].filter(Boolean);
|
|
403
407
|
lines.push(...planDiscoveryCaveat(
|
|
404
408
|
plans,
|