agentic-workflow-manager 9.20.4 → 9.22.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/dist/src/commands/change/index.js +14 -3
- package/dist/src/commands/preflight/checks.js +31 -0
- package/dist/src/commands/sensors/compatibility/contract.js +56 -3
- package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
- package/dist/src/commands/sensors/index.js +10 -1
- package/dist/src/commands/sensors/init.js +7 -1
- package/dist/src/commands/sensors/inputs-digest.js +315 -0
- package/dist/src/commands/sensors/prepare.js +10 -0
- package/dist/src/commands/sensors/result-cache.js +278 -0
- package/dist/src/commands/sensors/result.js +31 -12
- package/dist/src/commands/sensors/run.js +265 -8
- package/dist/src/commands/sensors/status.js +66 -7
- package/dist/src/commands/watch/init.js +3 -15
- package/dist/src/core/awm-gitignore.js +211 -0
- package/dist/src/core/changes/receipt.js +219 -9
- package/dist/src/core/diagnostics/checks.js +11 -0
- package/dist/tests/commands/change/new.test.js +1 -1
- package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
- package/dist/tests/commands/preflight/awm-gitignore.test.js +52 -0
- package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
- package/dist/tests/commands/sensors/init-gitignore.test.js +59 -0
- package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
- package/dist/tests/commands/sensors/result-cache.test.js +352 -0
- package/dist/tests/commands/sensors/run-cache.test.js +434 -0
- package/dist/tests/commands/sensors/run-slice.test.js +171 -0
- package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
- package/dist/tests/commands/watch/init-gitignore.test.js +34 -0
- package/dist/tests/commands/watch/track-freeze.test.js +11 -7
- package/dist/tests/commands/watch/watch-init.test.js +3 -1
- package/dist/tests/core/awm-gitignore.test.js +58 -0
- package/dist/tests/core/changes/receipt.test.js +261 -2
- package/dist/tests/core/diagnostics/awm-gitignore.test.js +101 -0
- package/dist/tests/core/ledger/gitignore.test.js +6 -1
- package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +102 -0
- package/dist/tests/structural/sensor-slice-decision-contract.test.js +63 -0
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AWM_LOCAL_PATHS = exports.AWM_VERSIONED_PATHS = exports.AWM_GITIGNORE_END = exports.AWM_GITIGNORE_BEGIN = void 0;
|
|
7
|
+
exports.renderManagedBlock = renderManagedBlock;
|
|
8
|
+
exports.upsertAwmGitignore = upsertAwmGitignore;
|
|
9
|
+
exports.evaluateAwmGitignorePolicy = evaluateAwmGitignorePolicy;
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
/** Marker lines for the managed `.gitignore` block (ASCII, exact). */
|
|
14
|
+
exports.AWM_GITIGNORE_BEGIN = '# AWM:GITIGNORE:BEGIN';
|
|
15
|
+
exports.AWM_GITIGNORE_END = '# AWM:GITIGNORE:END';
|
|
16
|
+
/** Project config under `.awm/` that must remain commit-able (D-024). */
|
|
17
|
+
exports.AWM_VERSIONED_PATHS = [
|
|
18
|
+
'.awm/sensors.json',
|
|
19
|
+
'.awm/sensors.baseline.json',
|
|
20
|
+
'.awm/profile.json',
|
|
21
|
+
'.awm/context-budget.json',
|
|
22
|
+
'.awm/context/index.json',
|
|
23
|
+
];
|
|
24
|
+
/** Runtime / local state under `.awm/` that must not be tracked (D-024). */
|
|
25
|
+
exports.AWM_LOCAL_PATHS = [
|
|
26
|
+
'.awm/journal',
|
|
27
|
+
'.awm/ledger',
|
|
28
|
+
'.awm/evidence',
|
|
29
|
+
'.awm/track.json',
|
|
30
|
+
'.awm/sensors/runs',
|
|
31
|
+
];
|
|
32
|
+
const MANAGED_LINES = [
|
|
33
|
+
'.awm/*',
|
|
34
|
+
'!.awm/sensors.json',
|
|
35
|
+
'!.awm/sensors.baseline.json',
|
|
36
|
+
'!.awm/profile.json',
|
|
37
|
+
'!.awm/context-budget.json',
|
|
38
|
+
'!.awm/context/',
|
|
39
|
+
'.awm/context/*',
|
|
40
|
+
'!.awm/context/index.json',
|
|
41
|
+
];
|
|
42
|
+
/** Canonical managed block including begin/end markers and a trailing newline. */
|
|
43
|
+
function renderManagedBlock() {
|
|
44
|
+
return [exports.AWM_GITIGNORE_BEGIN, ...MANAGED_LINES, exports.AWM_GITIGNORE_END, ''].join('\n');
|
|
45
|
+
}
|
|
46
|
+
function isBlanketAwmIgnore(line) {
|
|
47
|
+
const t = line.trim();
|
|
48
|
+
return t === '.awm' || t === '.awm/';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Upsert the managed AWM `.gitignore` block at the repository root.
|
|
52
|
+
* Idempotent when the block already matches. Migrates bare `.awm` / `.awm/`
|
|
53
|
+
* lines outside the block. Refuses a symlink `.gitignore`.
|
|
54
|
+
*/
|
|
55
|
+
function upsertAwmGitignore(repoRoot) {
|
|
56
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
|
|
57
|
+
throw new Error('upsertAwmGitignore requires a non-empty repository root');
|
|
58
|
+
}
|
|
59
|
+
const gi = path_1.default.join(repoRoot, '.gitignore');
|
|
60
|
+
let current = '';
|
|
61
|
+
try {
|
|
62
|
+
if (fs_1.default.lstatSync(gi).isSymbolicLink()) {
|
|
63
|
+
throw new Error('.gitignore must not be a symlink');
|
|
64
|
+
}
|
|
65
|
+
current = fs_1.default.readFileSync(gi, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error.code !== 'ENOENT')
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
const normalized = current.replace(/\r\n/g, '\n');
|
|
72
|
+
const block = renderManagedBlock();
|
|
73
|
+
const begin = normalized.indexOf(exports.AWM_GITIGNORE_BEGIN);
|
|
74
|
+
const end = normalized.indexOf(exports.AWM_GITIGNORE_END);
|
|
75
|
+
let withoutManaged;
|
|
76
|
+
if (begin !== -1 && end !== -1 && end > begin) {
|
|
77
|
+
const afterEnd = end + exports.AWM_GITIGNORE_END.length;
|
|
78
|
+
const skipNewline = normalized[afterEnd] === '\n' ? afterEnd + 1 : afterEnd;
|
|
79
|
+
withoutManaged = normalized.slice(0, begin) + normalized.slice(skipNewline);
|
|
80
|
+
}
|
|
81
|
+
else if (begin !== -1 || end !== -1) {
|
|
82
|
+
// Malformed markers: strip both marker lines and rebuild cleanly.
|
|
83
|
+
withoutManaged = normalized
|
|
84
|
+
.split('\n')
|
|
85
|
+
.filter((line) => line !== exports.AWM_GITIGNORE_BEGIN && line !== exports.AWM_GITIGNORE_END)
|
|
86
|
+
.join('\n');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
withoutManaged = normalized;
|
|
90
|
+
}
|
|
91
|
+
const preserved = withoutManaged
|
|
92
|
+
.split('\n')
|
|
93
|
+
.filter((line) => !isBlanketAwmIgnore(line))
|
|
94
|
+
.join('\n')
|
|
95
|
+
.replace(/\n+$/g, '');
|
|
96
|
+
const next = preserved.length > 0 ? `${preserved}\n${block}` : block;
|
|
97
|
+
if (next === normalized || next === current.replace(/\r\n/g, '\n')) {
|
|
98
|
+
// Already equivalent after normalize — avoid touching mtime when identical.
|
|
99
|
+
if (fs_1.default.existsSync(gi) && fs_1.default.readFileSync(gi, 'utf8').replace(/\r\n/g, '\n') === next)
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
fs_1.default.writeFileSync(gi, next);
|
|
103
|
+
}
|
|
104
|
+
function gitOk(repoRoot, args) {
|
|
105
|
+
try {
|
|
106
|
+
const stdout = (0, child_process_1.execFileSync)('git', args, {
|
|
107
|
+
cwd: repoRoot,
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
110
|
+
env: process.env,
|
|
111
|
+
});
|
|
112
|
+
return { ok: true, stdout };
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const err = error;
|
|
116
|
+
return { ok: false, code: typeof err.status === 'number' ? err.status : 1, stdout: typeof err.stdout === 'string' ? err.stdout : '' };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function isGitRepo(repoRoot) {
|
|
120
|
+
const result = gitOk(repoRoot, ['rev-parse', '--is-inside-work-tree']);
|
|
121
|
+
return result.ok && result.stdout.trim() === 'true';
|
|
122
|
+
}
|
|
123
|
+
function extractManagedInner(text) {
|
|
124
|
+
const begin = text.indexOf(exports.AWM_GITIGNORE_BEGIN);
|
|
125
|
+
const end = text.indexOf(exports.AWM_GITIGNORE_END);
|
|
126
|
+
if (begin === -1 || end === -1 || end <= begin)
|
|
127
|
+
return null;
|
|
128
|
+
return text.slice(begin + exports.AWM_GITIGNORE_BEGIN.length, end).replace(/^\n/, '').replace(/\n$/, '');
|
|
129
|
+
}
|
|
130
|
+
function managedBlockComplete(repoRoot) {
|
|
131
|
+
const gi = path_1.default.join(repoRoot, '.gitignore');
|
|
132
|
+
try {
|
|
133
|
+
const text = fs_1.default.readFileSync(gi, 'utf8').replace(/\r\n/g, '\n');
|
|
134
|
+
const inner = extractManagedInner(text);
|
|
135
|
+
if (inner === null)
|
|
136
|
+
return false;
|
|
137
|
+
const expected = MANAGED_LINES.join('\n');
|
|
138
|
+
return inner === expected;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error.code === 'ENOENT')
|
|
142
|
+
return false;
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function pathExistsUnderRoot(repoRoot, rel) {
|
|
147
|
+
try {
|
|
148
|
+
fs_1.default.lstatSync(path_1.default.join(repoRoot, rel));
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error.code === 'ENOENT')
|
|
153
|
+
return false;
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function awmTreePresent(repoRoot) {
|
|
158
|
+
return pathExistsUnderRoot(repoRoot, '.awm') || exports.AWM_VERSIONED_PATHS.some((p) => pathExistsUnderRoot(repoRoot, p));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Blocking policy check for doctor/preflight (GI-6..GI-8).
|
|
162
|
+
* Skips cleanly when not inside a git work tree.
|
|
163
|
+
*/
|
|
164
|
+
function evaluateAwmGitignorePolicy(repoRoot) {
|
|
165
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
|
|
166
|
+
throw new Error('evaluateAwmGitignorePolicy requires a non-empty repository root');
|
|
167
|
+
}
|
|
168
|
+
if (!isGitRepo(repoRoot)) {
|
|
169
|
+
return { ok: true, skipped: true, detail: 'skipped (not a git repository)' };
|
|
170
|
+
}
|
|
171
|
+
const trackedLocal = [];
|
|
172
|
+
for (const local of exports.AWM_LOCAL_PATHS) {
|
|
173
|
+
const listed = gitOk(repoRoot, ['ls-files', '-z', '--', local]);
|
|
174
|
+
if (!listed.ok)
|
|
175
|
+
continue;
|
|
176
|
+
const paths = listed.stdout.split('\0').filter(Boolean);
|
|
177
|
+
trackedLocal.push(...paths);
|
|
178
|
+
}
|
|
179
|
+
const ignoredVersioned = [];
|
|
180
|
+
for (const versioned of exports.AWM_VERSIONED_PATHS) {
|
|
181
|
+
if (!pathExistsUnderRoot(repoRoot, versioned))
|
|
182
|
+
continue;
|
|
183
|
+
const ignore = gitOk(repoRoot, ['check-ignore', '-q', '--', versioned]);
|
|
184
|
+
// exit 0 → ignored; exit 1 → not ignored; other → treat as not ignored (fail-open on check only)
|
|
185
|
+
if (ignore.ok || ignore.code === 0)
|
|
186
|
+
ignoredVersioned.push(versioned);
|
|
187
|
+
}
|
|
188
|
+
const needsBlock = awmTreePresent(repoRoot) || trackedLocal.length > 0;
|
|
189
|
+
const missingBlock = needsBlock && !managedBlockComplete(repoRoot);
|
|
190
|
+
if (trackedLocal.length === 0 && ignoredVersioned.length === 0 && !missingBlock) {
|
|
191
|
+
return { ok: true, detail: needsBlock ? 'managed .awm gitignore policy satisfied' : 'no .awm project state' };
|
|
192
|
+
}
|
|
193
|
+
const parts = [];
|
|
194
|
+
if (trackedLocal.length > 0) {
|
|
195
|
+
parts.push(`tracked local runtime: ${trackedLocal.slice(0, 8).join(', ')}${trackedLocal.length > 8 ? '…' : ''}`);
|
|
196
|
+
}
|
|
197
|
+
if (ignoredVersioned.length > 0) {
|
|
198
|
+
parts.push(`versioned config ignored: ${ignoredVersioned.join(', ')}`);
|
|
199
|
+
}
|
|
200
|
+
if (missingBlock) {
|
|
201
|
+
parts.push('managed # AWM:GITIGNORE block missing or incomplete');
|
|
202
|
+
}
|
|
203
|
+
const remedyParts = [
|
|
204
|
+
'run `awm sensors init` or `awm watch init` to upsert the managed .gitignore block',
|
|
205
|
+
];
|
|
206
|
+
if (trackedLocal.length > 0) {
|
|
207
|
+
const shown = trackedLocal.slice(0, 16).join(' ');
|
|
208
|
+
remedyParts.push(`then \`git rm --cached -- ${shown}${trackedLocal.length > 16 ? ' …' : ''}\``);
|
|
209
|
+
}
|
|
210
|
+
return { ok: false, detail: parts.join('; '), remedy: remedyParts.join('; ') };
|
|
211
|
+
}
|
|
@@ -5,6 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.isFullSelection = isFullSelection;
|
|
7
7
|
exports.sensorPairs = sensorPairs;
|
|
8
|
+
exports.allowsUnmeasuredRelaxation = allowsUnmeasuredRelaxation;
|
|
9
|
+
exports.measuredPathSet = measuredPathSet;
|
|
10
|
+
exports.filterLateFilesForReceipt = filterLateFilesForReceipt;
|
|
8
11
|
exports.computeReceiptDigest = computeReceiptDigest;
|
|
9
12
|
exports.buildReceipt = buildReceipt;
|
|
10
13
|
exports.writeReceipt = writeReceipt;
|
|
@@ -22,14 +25,20 @@ const paths_1 = require("../paths");
|
|
|
22
25
|
const types_1 = require("./types");
|
|
23
26
|
const text_safety_1 = require("./text-safety");
|
|
24
27
|
const worktree_1 = require("./worktree");
|
|
25
|
-
const
|
|
28
|
+
const inputs_digest_1 = require("../../commands/sensors/inputs-digest");
|
|
29
|
+
const contract_1 = require("../../commands/sensors/compatibility/contract");
|
|
30
|
+
const RECEIPT_KEYS_V1 = ['schema', 'commit', 'ranAt', 'cliVersion', 'overall', 'sensors', 'digest'];
|
|
31
|
+
const RECEIPT_KEYS_V2 = [...RECEIPT_KEYS_V1, 'measurements', 'alwaysMeasuredPaths'];
|
|
26
32
|
const MAX_RECEIPT_BYTES = 1024 * 1024;
|
|
27
|
-
|
|
33
|
+
const MAX_MEASURED_PATHS = 50_000;
|
|
34
|
+
/** True when the run covers every sensor: `--changed`/`--slice` never do, `--all` always does, `--fast` xor `--slow` is partial. */
|
|
28
35
|
function isFullSelection(opts) {
|
|
29
36
|
if (opts === null || typeof opts !== 'object')
|
|
30
37
|
return false;
|
|
31
38
|
if (opts.changed)
|
|
32
39
|
return false;
|
|
40
|
+
if (opts.slice)
|
|
41
|
+
return false;
|
|
33
42
|
if (opts.all)
|
|
34
43
|
return true;
|
|
35
44
|
return Boolean(opts.fast) === Boolean(opts.slow);
|
|
@@ -46,6 +55,108 @@ function sensorPairs(sensors) {
|
|
|
46
55
|
function sortedSensors(sensors) {
|
|
47
56
|
return Object.fromEntries(sensorPairs(sensors));
|
|
48
57
|
}
|
|
58
|
+
function sortedStrings(values) {
|
|
59
|
+
return [...values].sort(byName);
|
|
60
|
+
}
|
|
61
|
+
function isSha256Digest(v) {
|
|
62
|
+
return typeof v === 'string' && /^sha256:[0-9a-f]{64}$/.test(v);
|
|
63
|
+
}
|
|
64
|
+
function isPackageRootRel(v) {
|
|
65
|
+
return typeof v === 'string' && (v === '' || (0, inputs_digest_1.isSafeRepoRel)(v));
|
|
66
|
+
}
|
|
67
|
+
function measurementProblem(name, value) {
|
|
68
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
69
|
+
return `measurement for ${name} must be an object`;
|
|
70
|
+
const m = value;
|
|
71
|
+
const keys = Object.keys(m);
|
|
72
|
+
if (keys.length !== 5 || !['inputs', 'packageRootRel', 'inputDigest', 'sensorFingerprint', 'measuredPaths'].every((k) => k in m)) {
|
|
73
|
+
return `measurement for ${name} must have exactly inputs, packageRootRel, inputDigest, sensorFingerprint, measuredPaths`;
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(m.inputs) || m.inputs.length === 0) {
|
|
76
|
+
return `measurement inputs for ${name} must be a nonempty array of single-line strings`;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
// RF-1.5: reuse the same glob grammar as authored sensor inputs (reject .., !, braces, extglobs).
|
|
80
|
+
(0, contract_1.parseSensorInputs)(m.inputs, 'receipt', `measurement[${name}].inputs`);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return `measurement inputs for ${name} must obey RF-1.5 sensor input glob rules`;
|
|
84
|
+
}
|
|
85
|
+
if (!isPackageRootRel(m.packageRootRel))
|
|
86
|
+
return `measurement packageRootRel for ${name} must be '' or a safe repo-root POSIX path`;
|
|
87
|
+
if (!isSha256Digest(m.inputDigest))
|
|
88
|
+
return `measurement inputDigest for ${name} must be sha256:<hex>`;
|
|
89
|
+
if (!isSha256Digest(m.sensorFingerprint))
|
|
90
|
+
return `measurement sensorFingerprint for ${name} must be sha256:<hex>`;
|
|
91
|
+
if (!Array.isArray(m.measuredPaths) || m.measuredPaths.length > MAX_MEASURED_PATHS) {
|
|
92
|
+
return `measurement measuredPaths for ${name} must be an array within bounds`;
|
|
93
|
+
}
|
|
94
|
+
if (m.measuredPaths.some((p) => typeof p !== 'string' || !(0, inputs_digest_1.isSafeRepoRel)(p))) {
|
|
95
|
+
return `measurement measuredPaths for ${name} must be safe repo-root POSIX paths`;
|
|
96
|
+
}
|
|
97
|
+
const sorted = sortedStrings(m.measuredPaths);
|
|
98
|
+
if (JSON.stringify(m.measuredPaths) !== JSON.stringify(sorted)) {
|
|
99
|
+
return `measurement measuredPaths for ${name} must be sorted`;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* RF-4.6: schema-2 unmeasured-path relaxation applies only when every sensor on the
|
|
105
|
+
* receipt had effective authored inputs (a nonempty measurement.inputs entry).
|
|
106
|
+
*/
|
|
107
|
+
function allowsUnmeasuredRelaxation(receipt) {
|
|
108
|
+
if (receipt === null || typeof receipt !== 'object')
|
|
109
|
+
return false;
|
|
110
|
+
if (receipt.schema !== 2)
|
|
111
|
+
return false;
|
|
112
|
+
const names = Object.keys(receipt.sensors);
|
|
113
|
+
if (names.length === 0)
|
|
114
|
+
return false;
|
|
115
|
+
for (const name of names) {
|
|
116
|
+
const m = receipt.measurements[name];
|
|
117
|
+
if (!m || !Array.isArray(m.inputs) || m.inputs.length === 0)
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
/** Repo-root POSIX paths that count as measured for RF-4.5 late-path checks. */
|
|
123
|
+
function measuredPathSet(receipt) {
|
|
124
|
+
const out = new Set();
|
|
125
|
+
for (const p of receipt.alwaysMeasuredPaths)
|
|
126
|
+
out.add(p);
|
|
127
|
+
for (const m of Object.values(receipt.measurements)) {
|
|
128
|
+
for (const p of m.measuredPaths)
|
|
129
|
+
out.add(p);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
function isRuntimeStatePath(rel) {
|
|
134
|
+
return worktree_1.RUNTIME_STATE_EXCLUDES.some((ex) => rel === ex || rel.startsWith(`${ex}/`));
|
|
135
|
+
}
|
|
136
|
+
/** True when a late path is measured: frozen set OR rematch against any measurement's inputs (RF-4.5). */
|
|
137
|
+
function latePathIsMeasured(repoRel, receipt, frozen) {
|
|
138
|
+
if (frozen.has(repoRel))
|
|
139
|
+
return true;
|
|
140
|
+
for (const m of Object.values(receipt.measurements)) {
|
|
141
|
+
if ((0, inputs_digest_1.repoPathMatchesInputGlobs)(repoRel, m.packageRootRel, m.inputs))
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Filter late files for Change Record validate (RF-4.5/4.6). Schema 1 and incomplete
|
|
148
|
+
* schema 2 keep today's strict rule (only `docs/changes/` carved out). Complete schema 2
|
|
149
|
+
* keeps paths in the frozen measured ∪ always-measured union OR matching authored inputs
|
|
150
|
+
* globs (so new matching sources after verify are not a false green). Runtime-state never counts.
|
|
151
|
+
*/
|
|
152
|
+
function filterLateFilesForReceipt(files, receipt, recordDirPrefix) {
|
|
153
|
+
const outsideRecords = files.filter((f) => !f.startsWith(`${recordDirPrefix}/`));
|
|
154
|
+
if (receipt === null || receipt.schema !== 2 || !allowsUnmeasuredRelaxation(receipt)) {
|
|
155
|
+
return outsideRecords;
|
|
156
|
+
}
|
|
157
|
+
const measured = measuredPathSet(receipt);
|
|
158
|
+
return outsideRecords.filter((f) => !isRuntimeStatePath(f) && latePathIsMeasured(f, receipt, measured));
|
|
159
|
+
}
|
|
49
160
|
/**
|
|
50
161
|
* sha256 over the canonical JSON of the receipt without its digest, `sensors` being the name-sorted pairs of sensorPairs (R2.2c).
|
|
51
162
|
* The digest is local (receipts are not versioned and are rewritten by every full run), so changing its canonical form only
|
|
@@ -56,9 +167,33 @@ function computeReceiptDigest(body) {
|
|
|
56
167
|
throw new Error((0, text_safety_1.esc)('invalid receipt: digest body must be an object'));
|
|
57
168
|
if (body.sensors === null || typeof body.sensors !== 'object' || Array.isArray(body.sensors))
|
|
58
169
|
throw new Error((0, text_safety_1.esc)('invalid receipt: digest body sensors must be an object'));
|
|
170
|
+
if (body.schema === 1) {
|
|
171
|
+
const canonical = JSON.stringify({
|
|
172
|
+
schema: body.schema, commit: body.commit, ranAt: body.ranAt, cliVersion: body.cliVersion,
|
|
173
|
+
overall: body.overall, sensors: sensorPairs(body.sensors),
|
|
174
|
+
});
|
|
175
|
+
return 'sha256:' + crypto_1.default.createHash('sha256').update(canonical).digest('hex');
|
|
176
|
+
}
|
|
177
|
+
if (body.schema !== 2)
|
|
178
|
+
throw new Error((0, text_safety_1.esc)('invalid receipt: digest body schema must be 1 or 2'));
|
|
179
|
+
const v2 = body;
|
|
180
|
+
if (v2.measurements === null || typeof v2.measurements !== 'object' || Array.isArray(v2.measurements)) {
|
|
181
|
+
throw new Error((0, text_safety_1.esc)('invalid receipt: digest body measurements must be an object'));
|
|
182
|
+
}
|
|
183
|
+
if (!Array.isArray(v2.alwaysMeasuredPaths))
|
|
184
|
+
throw new Error((0, text_safety_1.esc)('invalid receipt: digest body alwaysMeasuredPaths must be an array'));
|
|
185
|
+
const measurements = Object.fromEntries(Object.entries(v2.measurements).sort(([a], [b]) => byName(a, b)).map(([name, m]) => [name, {
|
|
186
|
+
inputs: m.inputs,
|
|
187
|
+
packageRootRel: m.packageRootRel,
|
|
188
|
+
inputDigest: m.inputDigest,
|
|
189
|
+
sensorFingerprint: m.sensorFingerprint,
|
|
190
|
+
measuredPaths: m.measuredPaths,
|
|
191
|
+
}]));
|
|
59
192
|
const canonical = JSON.stringify({
|
|
60
|
-
schema:
|
|
61
|
-
overall:
|
|
193
|
+
schema: v2.schema, commit: v2.commit, ranAt: v2.ranAt, cliVersion: v2.cliVersion,
|
|
194
|
+
overall: v2.overall, sensors: sensorPairs(v2.sensors),
|
|
195
|
+
measurements,
|
|
196
|
+
alwaysMeasuredPaths: v2.alwaysMeasuredPaths,
|
|
62
197
|
});
|
|
63
198
|
return 'sha256:' + crypto_1.default.createHash('sha256').update(canonical).digest('hex');
|
|
64
199
|
}
|
|
@@ -70,11 +205,12 @@ function receiptProblem(x) {
|
|
|
70
205
|
if (x === null || typeof x !== 'object' || Array.isArray(x))
|
|
71
206
|
return 'receipt must be an object';
|
|
72
207
|
const r = x;
|
|
208
|
+
if (r.schema !== 1 && r.schema !== 2)
|
|
209
|
+
return 'schema must be 1 or 2';
|
|
210
|
+
const expectedKeys = r.schema === 1 ? RECEIPT_KEYS_V1 : RECEIPT_KEYS_V2;
|
|
73
211
|
const keys = Object.keys(r);
|
|
74
|
-
if (keys.length !==
|
|
212
|
+
if (keys.length !== expectedKeys.length || !expectedKeys.every((k) => k in r))
|
|
75
213
|
return 'receipt must have exactly the schema keys';
|
|
76
|
-
if (r.schema !== 1)
|
|
77
|
-
return 'schema must be 1';
|
|
78
214
|
if (!(0, types_1.isCommitId)(r.commit))
|
|
79
215
|
return 'commit must be 40 or 64 lowercase hex characters';
|
|
80
216
|
if (!(0, text_safety_1.isCanonicalIso)(r.ranAt))
|
|
@@ -92,6 +228,28 @@ function receiptProblem(x) {
|
|
|
92
228
|
if (!(0, types_1.isSensorStatus)(status))
|
|
93
229
|
return 'a sensor status is not a known status';
|
|
94
230
|
}
|
|
231
|
+
if (r.schema === 2) {
|
|
232
|
+
const measurements = r.measurements;
|
|
233
|
+
if (measurements === null || typeof measurements !== 'object' || Array.isArray(measurements))
|
|
234
|
+
return 'measurements must be an object';
|
|
235
|
+
for (const [name, value] of Object.entries(measurements)) {
|
|
236
|
+
if (!(0, types_1.isSensorName)(name))
|
|
237
|
+
return `measurement sensor names must be a single line of 1 to ${types_1.MAX_SENSOR_NAME} characters`;
|
|
238
|
+
const problem = measurementProblem(name, value);
|
|
239
|
+
if (problem !== null)
|
|
240
|
+
return problem;
|
|
241
|
+
}
|
|
242
|
+
if (!Array.isArray(r.alwaysMeasuredPaths) || r.alwaysMeasuredPaths.length > MAX_MEASURED_PATHS) {
|
|
243
|
+
return 'alwaysMeasuredPaths must be an array within bounds';
|
|
244
|
+
}
|
|
245
|
+
if (r.alwaysMeasuredPaths.some((p) => typeof p !== 'string' || !(0, inputs_digest_1.isSafeRepoRel)(p))) {
|
|
246
|
+
return 'alwaysMeasuredPaths must be safe repo-root POSIX paths';
|
|
247
|
+
}
|
|
248
|
+
const sortedAlways = sortedStrings(r.alwaysMeasuredPaths);
|
|
249
|
+
if (JSON.stringify(r.alwaysMeasuredPaths) !== JSON.stringify(sortedAlways)) {
|
|
250
|
+
return 'alwaysMeasuredPaths must be sorted';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
95
253
|
if (typeof r.digest !== 'string')
|
|
96
254
|
return 'digest must be a string';
|
|
97
255
|
const { digest, ...body } = r;
|
|
@@ -99,14 +257,66 @@ function receiptProblem(x) {
|
|
|
99
257
|
return 'digest does not match the content';
|
|
100
258
|
return null;
|
|
101
259
|
}
|
|
102
|
-
|
|
260
|
+
function extractMeasurements(sensors) {
|
|
261
|
+
const out = {};
|
|
262
|
+
for (const sensor of sensors) {
|
|
263
|
+
const m = sensor.measurement;
|
|
264
|
+
if (!m || typeof m !== 'object')
|
|
265
|
+
continue;
|
|
266
|
+
if (!Array.isArray(m.inputs) || m.inputs.length === 0)
|
|
267
|
+
continue;
|
|
268
|
+
if (!isPackageRootRel(m.packageRootRel))
|
|
269
|
+
continue;
|
|
270
|
+
if (!isSha256Digest(m.inputDigest) || !isSha256Digest(m.sensorFingerprint))
|
|
271
|
+
continue;
|
|
272
|
+
if (!Array.isArray(m.measuredPaths) || m.measuredPaths.some((p) => typeof p !== 'string' || !(0, inputs_digest_1.isSafeRepoRel)(p)))
|
|
273
|
+
continue;
|
|
274
|
+
out[sensor.name] = {
|
|
275
|
+
inputs: [...m.inputs],
|
|
276
|
+
packageRootRel: m.packageRootRel,
|
|
277
|
+
inputDigest: m.inputDigest,
|
|
278
|
+
sensorFingerprint: m.sensorFingerprint,
|
|
279
|
+
measuredPaths: sortedStrings(m.measuredPaths),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => byName(a, b)));
|
|
283
|
+
}
|
|
284
|
+
/** Builds a schema-2 receipt (RF-4.3). Schema 1 remains readable via `readReceipt` / `writeReceipt`. */
|
|
103
285
|
function buildReceipt(input) {
|
|
104
286
|
const out = input === null || typeof input !== 'object' ? undefined : input.output;
|
|
105
287
|
if (out === null || typeof out !== 'object' || !Array.isArray(out.sensors) || out.sensors.some((s) => s === null || typeof s !== 'object')) {
|
|
106
288
|
throw new Error('invalid receipt: run output must carry a sensors array');
|
|
107
289
|
}
|
|
108
290
|
const sensors = sortedSensors(Object.fromEntries(out.sensors.map((s) => [s.name, s.status])));
|
|
109
|
-
const
|
|
291
|
+
const measurements = extractMeasurements(out.sensors);
|
|
292
|
+
const alwaysFromSensors = new Set();
|
|
293
|
+
for (const sensor of out.sensors) {
|
|
294
|
+
const extras = sensor.measurement?.alwaysMeasuredPaths;
|
|
295
|
+
if (!Array.isArray(extras))
|
|
296
|
+
continue;
|
|
297
|
+
for (const p of extras) {
|
|
298
|
+
if (typeof p === 'string' && (0, inputs_digest_1.isSafeRepoRel)(p))
|
|
299
|
+
alwaysFromSensors.add(p);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const provided = Array.isArray(input.alwaysMeasuredPaths) ? input.alwaysMeasuredPaths : [];
|
|
303
|
+
for (const p of provided) {
|
|
304
|
+
if (typeof p === 'string' && (0, inputs_digest_1.isSafeRepoRel)(p))
|
|
305
|
+
alwaysFromSensors.add(p);
|
|
306
|
+
}
|
|
307
|
+
// Validate always-measured always includes baseline (RF-2.12 / RF-4.5), even when empty.
|
|
308
|
+
alwaysFromSensors.add(inputs_digest_1.CACHE_DIGEST_BASELINE_REL);
|
|
309
|
+
const alwaysMeasuredPaths = sortedStrings([...alwaysFromSensors]);
|
|
310
|
+
const body = {
|
|
311
|
+
schema: 2,
|
|
312
|
+
commit: input.commit,
|
|
313
|
+
ranAt: input.ranAt,
|
|
314
|
+
cliVersion: input.cliVersion,
|
|
315
|
+
overall: out.overall,
|
|
316
|
+
sensors,
|
|
317
|
+
measurements,
|
|
318
|
+
alwaysMeasuredPaths,
|
|
319
|
+
};
|
|
110
320
|
const receipt = { ...body, digest: computeReceiptDigest(body) };
|
|
111
321
|
const problem = receiptProblem(receipt);
|
|
112
322
|
if (problem !== null)
|
|
@@ -7,6 +7,7 @@ exports.computeProviderOverall = computeProviderOverall;
|
|
|
7
7
|
exports.runChecks = runChecks;
|
|
8
8
|
// src/core/diagnostics/checks.ts
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const awm_gitignore_1 = require("../awm-gitignore");
|
|
10
11
|
const cmd = (value) => ({ kind: 'command', value });
|
|
11
12
|
const skillRemedy = (value) => ({ kind: 'skill', value });
|
|
12
13
|
const none = { kind: 'none' };
|
|
@@ -152,6 +153,16 @@ function projectChecks(p) {
|
|
|
152
153
|
out.push({ id: 'project.context', level: 'project', label: 'agent context (CLAUDE.md/AGENTS.md) missing', status: 'warn',
|
|
153
154
|
remedy: skillRemedy('project-context-init') });
|
|
154
155
|
}
|
|
156
|
+
// project.awm-gitignore — D-024 / #218 (blocking via status missing)
|
|
157
|
+
const gi = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(p.root);
|
|
158
|
+
if (gi.ok) {
|
|
159
|
+
out.push({ id: 'project.awm-gitignore', level: 'project', label: '.awm gitignore policy',
|
|
160
|
+
status: 'ok', detail: gi.detail, remedy: none });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
out.push({ id: 'project.awm-gitignore', level: 'project', label: '.awm gitignore policy',
|
|
164
|
+
status: 'missing', detail: gi.detail, remedy: cmd(gi.remedy) });
|
|
165
|
+
}
|
|
155
166
|
return out;
|
|
156
167
|
}
|
|
157
168
|
// --- Task 9: overall status for the per-provider diagnostic matrix --------
|
|
@@ -62,7 +62,7 @@ let repo;
|
|
|
62
62
|
const now = () => new Date('2026-10-17T12:00:00.000Z');
|
|
63
63
|
const pass = { overall: 'pass', sensors: [{ name: 'lint', status: 'pass', errors: [] }] };
|
|
64
64
|
const RECEIPT_TIME = '2026-10-17T11:00:00.000Z';
|
|
65
|
-
const REMEDY = 'commit your work, then run awm sensors run';
|
|
65
|
+
const REMEDY = 'commit your work, then run full-gate awm sensors run (cache may reuse unchanged sensors)';
|
|
66
66
|
const REMEDY_SPLIT = 'split the change into smaller ones';
|
|
67
67
|
const REMEDY_SENSORS = 'reduce the sensors listed in .awm/sensors.json (the sensor receipt makes the record too large)';
|
|
68
68
|
function receiptForHead() {
|