@sabaiway/agent-workflow-kit 13.1.0 → 13.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 +140 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/review-state.md +1 -1
- package/references/modes/upgrade.md +4 -4
- package/references/templates/vehicles.json +8 -0
- package/tools/checkpoint-core.mjs +118 -0
- package/tools/checkpoint-restore.mjs +126 -0
- package/tools/checkpoint.mjs +146 -0
- package/tools/ensure-configs.mjs +8 -7
- package/tools/ensure-ops.mjs +15 -7
- package/tools/ensure-vocabulary.mjs +1 -1
- package/tools/plan-files.mjs +2 -1
- package/tools/review-state.mjs +1 -1
- package/tools/task-brief.mjs +270 -0
- package/tools/vehicle-settings.mjs +127 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readFileSync, lstatSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const VEHICLES_REL = 'docs/ai/vehicles.json';
|
|
5
|
+
export const EXECUTOR_DEFAULTS = Object.freeze({ model: 'opus', effort: 'high', fallback: 'sonnet' });
|
|
6
|
+
|
|
7
|
+
const README_KEY = '_README';
|
|
8
|
+
const EXECUTOR_KEY = 'executor';
|
|
9
|
+
const TOP_LEVEL_KEYS = Object.freeze([README_KEY, EXECUTOR_KEY]);
|
|
10
|
+
const EXECUTOR_KEYS = Object.freeze(Object.keys(EXECUTOR_DEFAULTS));
|
|
11
|
+
const OBJECT_TYPE = 'object';
|
|
12
|
+
const STRING_TYPE = 'string';
|
|
13
|
+
const EMPTY_STRING = '';
|
|
14
|
+
const TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
15
|
+
const ABSENT = 'absent';
|
|
16
|
+
const PRESENT = 'present';
|
|
17
|
+
const UNREADABLE = 'unreadable';
|
|
18
|
+
const FILE_SOURCE = 'file';
|
|
19
|
+
const DEFAULT_SOURCE = 'default';
|
|
20
|
+
const MISSING_ENTRY_CODE = 'ENOENT';
|
|
21
|
+
const ENCODING = 'utf8';
|
|
22
|
+
const CONFIG_EXIT_CODE = 1;
|
|
23
|
+
const BODY_NAME = 'body';
|
|
24
|
+
const UNKNOWN_KEY = 'unknown key';
|
|
25
|
+
const REQUIRED_KEY = 'required key is missing';
|
|
26
|
+
const OBJECT_REQUIRED = 'must be a plain object';
|
|
27
|
+
const STRING_REQUIRED = 'must be a string';
|
|
28
|
+
const NONEMPTY_REQUIRED = 'must not be empty';
|
|
29
|
+
const TOKEN_REQUIRED = 'must be a bare token';
|
|
30
|
+
const DISTINCT_FALLBACK_REQUIRED = 'fallback must differ from model';
|
|
31
|
+
const DIRECTORY_KIND = 'directory';
|
|
32
|
+
const SYMLINK_KIND = 'symlink';
|
|
33
|
+
const OTHER_KIND = 'not a regular file';
|
|
34
|
+
const UNKNOWN_STATE = 'unknown state';
|
|
35
|
+
|
|
36
|
+
const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
37
|
+
const isPlainObject = (value) => {
|
|
38
|
+
if (value === null || typeof value !== OBJECT_TYPE || Array.isArray(value)) return false;
|
|
39
|
+
const prototype = Object.getPrototypeOf(value);
|
|
40
|
+
return prototype === Object.prototype || prototype === null;
|
|
41
|
+
};
|
|
42
|
+
const refuseValue = (rel, key, problem) => ({ ok: false, reason: `${rel}: ${key} ${problem}` });
|
|
43
|
+
const unreadable = (reason) => ({ state: UNREADABLE, reason });
|
|
44
|
+
const describeError = (error) => String(error?.code ?? error?.message ?? error);
|
|
45
|
+
const formatReadError = (error) => `${VEHICLES_REL}: ${UNREADABLE} (${describeError(error)})`;
|
|
46
|
+
|
|
47
|
+
export const validateVehicles = (value, rel = VEHICLES_REL) => {
|
|
48
|
+
if (!isPlainObject(value)) return refuseValue(rel, BODY_NAME, OBJECT_REQUIRED);
|
|
49
|
+
for (const key of Object.keys(value)) {
|
|
50
|
+
if (!TOP_LEVEL_KEYS.includes(key)) return refuseValue(rel, key, UNKNOWN_KEY);
|
|
51
|
+
}
|
|
52
|
+
if (Object.hasOwn(value, README_KEY) && typeof value[README_KEY] !== STRING_TYPE) {
|
|
53
|
+
return refuseValue(rel, README_KEY, STRING_REQUIRED);
|
|
54
|
+
}
|
|
55
|
+
if (!Object.hasOwn(value, EXECUTOR_KEY)) return refuseValue(rel, EXECUTOR_KEY, REQUIRED_KEY);
|
|
56
|
+
const executor = value[EXECUTOR_KEY];
|
|
57
|
+
if (!isPlainObject(executor)) return refuseValue(rel, EXECUTOR_KEY, OBJECT_REQUIRED);
|
|
58
|
+
for (const key of Object.keys(executor)) {
|
|
59
|
+
if (!EXECUTOR_KEYS.includes(key)) return refuseValue(rel, `${EXECUTOR_KEY}.${key}`, UNKNOWN_KEY);
|
|
60
|
+
}
|
|
61
|
+
for (const key of EXECUTOR_KEYS) {
|
|
62
|
+
const qualifiedKey = `${EXECUTOR_KEY}.${key}`;
|
|
63
|
+
if (!Object.hasOwn(executor, key)) return refuseValue(rel, qualifiedKey, REQUIRED_KEY);
|
|
64
|
+
const token = executor[key];
|
|
65
|
+
if (typeof token !== STRING_TYPE) return refuseValue(rel, qualifiedKey, STRING_REQUIRED);
|
|
66
|
+
if (token === EMPTY_STRING) return refuseValue(rel, qualifiedKey, NONEMPTY_REQUIRED);
|
|
67
|
+
if (!TOKEN_PATTERN.test(token) || token.trim() !== token) {
|
|
68
|
+
return refuseValue(rel, qualifiedKey, TOKEN_REQUIRED);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (executor.fallback === executor.model) {
|
|
72
|
+
return refuseValue(rel, EXECUTOR_KEY, DISTINCT_FALLBACK_REQUIRED);
|
|
73
|
+
}
|
|
74
|
+
const { model, effort, fallback } = executor;
|
|
75
|
+
return { ok: true, settings: { executor: { model, effort, fallback } } };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const probeEntry = (path, lstat) => {
|
|
79
|
+
try {
|
|
80
|
+
return { entry: lstat(path) };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return error?.code === MISSING_ENTRY_CODE
|
|
83
|
+
? { state: ABSENT }
|
|
84
|
+
: unreadable(formatReadError(error));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const describeNodeKind = (entry) => {
|
|
88
|
+
if (entry.isSymbolicLink()) return SYMLINK_KIND;
|
|
89
|
+
if (entry.isDirectory()) return DIRECTORY_KIND;
|
|
90
|
+
return OTHER_KIND;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const readVehicles = (cwd, readFile = readFileSync, lstat = lstatSync) => {
|
|
94
|
+
try {
|
|
95
|
+
const path = join(cwd, VEHICLES_REL);
|
|
96
|
+
const probe = probeEntry(path, lstat);
|
|
97
|
+
if (probe.state) return probe;
|
|
98
|
+
const { entry } = probe;
|
|
99
|
+
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
100
|
+
return unreadable(`${VEHICLES_REL}: ${describeNodeKind(entry)}`);
|
|
101
|
+
}
|
|
102
|
+
const value = JSON.parse(readFile(path, ENCODING));
|
|
103
|
+
const validation = validateVehicles(value);
|
|
104
|
+
return validation.ok
|
|
105
|
+
? { state: PRESENT, settings: validation.settings }
|
|
106
|
+
: unreadable(validation.reason);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return unreadable(formatReadError(error));
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const loadVehicles = (cwd, readFile = readFileSync, lstat = lstatSync) => {
|
|
113
|
+
const answer = readVehicles(cwd, readFile, lstat);
|
|
114
|
+
if (answer.state === UNREADABLE) throw fail(CONFIG_EXIT_CODE, answer.reason);
|
|
115
|
+
return answer;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const resolveExecutor = (answer) => {
|
|
119
|
+
if (answer?.state === PRESENT) {
|
|
120
|
+
return { posture: { ...answer.settings.executor, source: FILE_SOURCE }, reason: null };
|
|
121
|
+
}
|
|
122
|
+
if (answer?.state === ABSENT) {
|
|
123
|
+
return { posture: { ...EXECUTOR_DEFAULTS, source: DEFAULT_SOURCE }, reason: null };
|
|
124
|
+
}
|
|
125
|
+
if (answer?.state === UNREADABLE) return { posture: null, reason: answer.reason };
|
|
126
|
+
throw fail(CONFIG_EXIT_CODE, `${VEHICLES_REL}: ${UNKNOWN_STATE} ${String(answer?.state)}`);
|
|
127
|
+
};
|