agentic-workflow-manager 9.20.3 → 9.21.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/preflight/checks.js +9 -0
- package/dist/src/commands/sensors/init.js +7 -1
- package/dist/src/commands/sensors/status.js +6 -1
- package/dist/src/commands/watch/init.js +3 -15
- package/dist/src/core/awm-gitignore.js +211 -0
- package/dist/src/core/diagnostics/checks.js +11 -0
- package/dist/tests/commands/preflight/awm-gitignore.test.js +52 -0
- package/dist/tests/commands/sensors/init-gitignore.test.js +59 -0
- package/dist/tests/commands/sensors/status.test.js +62 -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/diagnostics/awm-gitignore.test.js +101 -0
- package/dist/tests/core/ledger/gitignore.test.js +6 -1
- 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
|
@@ -19,6 +19,7 @@ const registries_2 = require("../../core/registries");
|
|
|
19
19
|
const check_1 = require("../../core/currentness/check");
|
|
20
20
|
const store_1 = require("../../core/model-policy/store");
|
|
21
21
|
const setup_readiness_1 = require("../../core/model-policy/setup-readiness");
|
|
22
|
+
const awm_gitignore_1 = require("../../core/awm-gitignore");
|
|
22
23
|
const MANIFEST = path_1.default.join('.awm', 'sensors.json');
|
|
23
24
|
function checkRoutingSetup(cwd) {
|
|
24
25
|
const guidance = (0, setup_readiness_1.routingSetupGuidance)((0, store_1.readEffectivePolicy)(cwd));
|
|
@@ -441,6 +442,12 @@ function extractHost(remote) {
|
|
|
441
442
|
* a PR/MR at all (merge locally, keep the branch as-is), so gating the whole harness on
|
|
442
443
|
* missing tooling here would be wrong. It is FYI, never a blocker.
|
|
443
444
|
*/
|
|
445
|
+
function checkAwmGitignore(cwd) {
|
|
446
|
+
const result = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(path_1.default.resolve(cwd));
|
|
447
|
+
if (result.ok)
|
|
448
|
+
return { id: 'awm-gitignore', ok: true, detail: result.detail };
|
|
449
|
+
return { id: 'awm-gitignore', ok: false, detail: result.detail, remedy: result.remedy };
|
|
450
|
+
}
|
|
444
451
|
function checkHost(cwd) {
|
|
445
452
|
let remote;
|
|
446
453
|
try {
|
|
@@ -540,6 +547,8 @@ async function preflight(cwd = process.cwd(), opts = {}) {
|
|
|
540
547
|
// Strict mode is the only path that contacts authoritative npm/Git sources.
|
|
541
548
|
...(opts.requireCurrent === true ? [checkCompatibility()] : []),
|
|
542
549
|
...checkRoutingSetup(cwd),
|
|
550
|
+
// Blocking .awm versioning policy (D-024 / #218).
|
|
551
|
+
checkAwmGitignore(cwd),
|
|
543
552
|
// Runs unconditionally — orthogonal to sensor configuration entirely, this is
|
|
544
553
|
// about PR/MR tooling, not sensors.
|
|
545
554
|
checkHost(cwd),
|
|
@@ -12,6 +12,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
12
12
|
const pack_source_1 = require("./compatibility/pack-source");
|
|
13
13
|
const bootstrap_1 = require("./bootstrap");
|
|
14
14
|
const unresolved_1 = require("./unresolved");
|
|
15
|
+
const awm_gitignore_1 = require("../../core/awm-gitignore");
|
|
15
16
|
/** Read a pack only through the shared source resolver. Init is a registry
|
|
16
17
|
* boundary: a pack path that is absent is harmless, but a symlink or escaping
|
|
17
18
|
* claimed source is never silently downgraded to an empty manifest. */
|
|
@@ -165,12 +166,17 @@ async function initSensors(opts = {}) {
|
|
|
165
166
|
}
|
|
166
167
|
if (plan.kind === 'migrate')
|
|
167
168
|
throw new Error('initSensors does not migrate existing v2 manifests; run awm sensors bootstrap');
|
|
168
|
-
if (plan.kind === 'noop')
|
|
169
|
+
if (plan.kind === 'noop') {
|
|
170
|
+
// Repair path: existing project `.awm/` should still get the managed block (GI-1 remedy).
|
|
171
|
+
if (fs_1.default.existsSync(path_1.default.join(cwd, '.awm')))
|
|
172
|
+
(0, awm_gitignore_1.upsertAwmGitignore)(cwd);
|
|
169
173
|
return {
|
|
170
174
|
// No manifest is written or synthesized here. The compatibility API only
|
|
171
175
|
// retains its historical result shape for callers that inspect detection.
|
|
172
176
|
manifest: {}, detection: detectStack(detectionCwd), configured: [], status: 'already-configured',
|
|
173
177
|
};
|
|
178
|
+
}
|
|
174
179
|
(0, bootstrap_1.applySensorBootstrap)(plan);
|
|
180
|
+
(0, awm_gitignore_1.upsertAwmGitignore)(cwd);
|
|
175
181
|
return { manifest: plan.manifest, detection: detectStack(detectionCwd), configured: [], status: 'created', ...(plan.unresolved && plan.unresolved.length > 0 ? { unresolved: plan.unresolved } : {}) };
|
|
176
182
|
}
|
|
@@ -215,7 +215,12 @@ function projectSensorCheck(sensor, cwd) {
|
|
|
215
215
|
function packBoundOnly(manifest) {
|
|
216
216
|
const sensors = Object.fromEntries(Object.entries(manifest.sensors).filter(([, sensor]) => !(0, manifest_1.isProjectDeclaredSensor)(sensor)));
|
|
217
217
|
const pack = 'pack' in manifest && typeof manifest.pack === 'string' ? manifest.pack : '';
|
|
218
|
-
return {
|
|
218
|
+
return {
|
|
219
|
+
schemaVersion: 2,
|
|
220
|
+
pack,
|
|
221
|
+
sensors,
|
|
222
|
+
...(manifest.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}),
|
|
223
|
+
};
|
|
219
224
|
}
|
|
220
225
|
/** Live pack sensor ids, or null when the pack cannot be resolved (fail closed). */
|
|
221
226
|
function livePackSensorIds(source) {
|
|
@@ -18,6 +18,7 @@ const lock_1 = require("./lock");
|
|
|
18
18
|
const validate_1 = require("../../core/plan/validate");
|
|
19
19
|
const identity_1 = require("../../core/plan/identity");
|
|
20
20
|
const native_bridge_1 = require("../../core/secure-fs/native-bridge");
|
|
21
|
+
const awm_gitignore_1 = require("../../core/awm-gitignore");
|
|
21
22
|
const MAX_VERIFIER_SCAN_DEPTH = 64;
|
|
22
23
|
const MAX_VERIFIER_SCAN_ENTRIES = 10000;
|
|
23
24
|
const MAX_PACKAGE_JSON_BYTES = 256 * 1024;
|
|
@@ -81,22 +82,9 @@ function detectRequiredVerifiers(repoRoot) {
|
|
|
81
82
|
visit(repoRoot, 0);
|
|
82
83
|
return ['test', 'sensors'].filter((kind) => kinds.has(kind));
|
|
83
84
|
}
|
|
84
|
-
/** El journal es gitignoreado (R1.1):
|
|
85
|
+
/** El journal es gitignoreado (R1.1): bloque gestionado D-024 / #218. */
|
|
85
86
|
function ensureJournalGitignored(repoRoot) {
|
|
86
|
-
|
|
87
|
-
let current = '';
|
|
88
|
-
try {
|
|
89
|
-
if (fs_1.default.lstatSync(gi).isSymbolicLink())
|
|
90
|
-
throw new Error('.gitignore must not be a symlink');
|
|
91
|
-
current = fs_1.default.readFileSync(gi, 'utf8');
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
if (error.code !== 'ENOENT')
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
if (!current.split('\n').some((l) => l.trim() === '.awm/' || l.trim() === '.awm')) {
|
|
98
|
-
fs_1.default.writeFileSync(gi, current.length > 0 && !current.endsWith('\n') ? `${current}\n.awm/\n` : `${current}.awm/\n`);
|
|
99
|
-
}
|
|
87
|
+
(0, awm_gitignore_1.upsertAwmGitignore)(repoRoot);
|
|
100
88
|
}
|
|
101
89
|
const TERMINAL_JOB_STATES = new Set(['exited', 'cancelled']);
|
|
102
90
|
function rebindBlocker(state) {
|
|
@@ -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
|
+
}
|
|
@@ -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 --------
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const checks_1 = require("../../../src/commands/preflight/checks");
|
|
11
|
+
const awm_gitignore_1 = require("../../../src/core/awm-gitignore");
|
|
12
|
+
function git(cwd, args) {
|
|
13
|
+
(0, child_process_1.execFileSync)('git', args, { cwd, stdio: 'pipe' });
|
|
14
|
+
}
|
|
15
|
+
describe('preflight awm-gitignore check (GI-6/GI-7)', () => {
|
|
16
|
+
let repo;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-gi-pf-'));
|
|
19
|
+
git(repo, ['init']);
|
|
20
|
+
git(repo, ['config', 'user.email', 't@t']);
|
|
21
|
+
git(repo, ['config', 'user.name', 't']);
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
it('degrades when versioned sensors.json is ignored', async () => {
|
|
27
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
|
|
28
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), JSON.stringify({
|
|
29
|
+
schemaVersion: 3, mode: 'opt-out', reason: 'test',
|
|
30
|
+
}));
|
|
31
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), '.awm/\n');
|
|
32
|
+
const report = await (0, checks_1.preflight)(repo);
|
|
33
|
+
const check = report.checks.find((c) => c.id === 'awm-gitignore');
|
|
34
|
+
expect(check).toBeDefined();
|
|
35
|
+
expect(check.ok).toBe(false);
|
|
36
|
+
expect(check.advisory).not.toBe(true);
|
|
37
|
+
expect(check.remedy).toMatch(/awm sensors init|awm watch init/);
|
|
38
|
+
expect(report.status).not.toBe('ready');
|
|
39
|
+
});
|
|
40
|
+
it('includes a passing awm-gitignore check when managed block is present for opt-out manifest', async () => {
|
|
41
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
|
|
42
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), JSON.stringify({
|
|
43
|
+
schemaVersion: 3, mode: 'opt-out', reason: 'test',
|
|
44
|
+
}));
|
|
45
|
+
(0, awm_gitignore_1.upsertAwmGitignore)(repo);
|
|
46
|
+
git(repo, ['add', '-A']);
|
|
47
|
+
git(repo, ['commit', '-m', 'ok']);
|
|
48
|
+
const report = await (0, checks_1.preflight)(repo);
|
|
49
|
+
const check = report.checks.find((c) => c.id === 'awm-gitignore');
|
|
50
|
+
expect(check).toMatchObject({ ok: true });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const awm_gitignore_1 = require("../../../src/core/awm-gitignore");
|
|
10
|
+
jest.mock('../../../src/commands/sensors/bootstrap', () => ({
|
|
11
|
+
planSensorBootstrap: jest.fn(),
|
|
12
|
+
applySensorBootstrap: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
jest.mock('../../../src/commands/sensors/detection', () => ({
|
|
15
|
+
detectStack: jest.fn(() => ({ pack: 'js-ts', evidence: [] })),
|
|
16
|
+
}));
|
|
17
|
+
const { planSensorBootstrap, applySensorBootstrap } = require('../../../src/commands/sensors/bootstrap');
|
|
18
|
+
const { initSensors } = require('../../../src/commands/sensors/init');
|
|
19
|
+
describe('initSensors managed awm gitignore (GI-1)', () => {
|
|
20
|
+
let tmp;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sensors-gi-'));
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
it('upserts the managed block after a successful create', async () => {
|
|
29
|
+
planSensorBootstrap.mockResolvedValue({
|
|
30
|
+
kind: 'create',
|
|
31
|
+
projectRoot: tmp,
|
|
32
|
+
manifestPath: path_1.default.join(tmp, '.awm', 'sensors.json'),
|
|
33
|
+
dryRun: false,
|
|
34
|
+
changes: [],
|
|
35
|
+
manifest: { schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' }, sensors: {} },
|
|
36
|
+
});
|
|
37
|
+
applySensorBootstrap.mockImplementation(() => {
|
|
38
|
+
fs_1.default.mkdirSync(path_1.default.join(tmp, '.awm'), { recursive: true });
|
|
39
|
+
fs_1.default.writeFileSync(path_1.default.join(tmp, '.awm', 'sensors.json'), '{}');
|
|
40
|
+
return 'created';
|
|
41
|
+
});
|
|
42
|
+
await initSensors({ cwd: tmp });
|
|
43
|
+
expect(applySensorBootstrap).toHaveBeenCalled();
|
|
44
|
+
const gi = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
|
|
45
|
+
expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
|
|
46
|
+
expect(gi).toContain('!.awm/profile.json');
|
|
47
|
+
});
|
|
48
|
+
it('repairs the managed block on noop when .awm/ already exists', async () => {
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.join(tmp, '.awm'), { recursive: true });
|
|
50
|
+
fs_1.default.writeFileSync(path_1.default.join(tmp, '.awm', 'sensors.json'), '{}');
|
|
51
|
+
fs_1.default.writeFileSync(path_1.default.join(tmp, '.gitignore'), '.awm/\n');
|
|
52
|
+
planSensorBootstrap.mockResolvedValue({ kind: 'noop' });
|
|
53
|
+
await initSensors({ cwd: tmp });
|
|
54
|
+
expect(applySensorBootstrap).not.toHaveBeenCalled();
|
|
55
|
+
const gi = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
|
|
56
|
+
expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
|
|
57
|
+
expect(gi.split('\n').map((l) => l.trim()).filter((l) => l === '.awm/')).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -268,6 +268,68 @@ describe('computeSensorStatus', () => {
|
|
|
268
268
|
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
269
269
|
}
|
|
270
270
|
});
|
|
271
|
+
it('keeps packSelection explicit through status so explicit-opt-in sensors do not false-drift', async () => {
|
|
272
|
+
// Regression for #231: packBoundOnly used to drop packSelection before
|
|
273
|
+
// resolveStaticV2Compatibility. Without it, applies() leaves explicit-opt-in
|
|
274
|
+
// sensors not-applicable and status reports "compatibility drift: initialized
|
|
275
|
+
// prettier, resolved not-applicable" even though run() (which keeps the field)
|
|
276
|
+
// would certify the same manifest.
|
|
277
|
+
const previousHome = process.env.AWM_HOME;
|
|
278
|
+
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-home-'));
|
|
279
|
+
try {
|
|
280
|
+
process.env.AWM_HOME = home;
|
|
281
|
+
const registry = path_1.default.join(home, 'registries', 'baseline');
|
|
282
|
+
fs_1.default.mkdirSync(path_1.default.join(registry, 'sensor-packs', 'js-ts'), { recursive: true });
|
|
283
|
+
fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
|
|
284
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
|
|
285
|
+
schemaVersion: 2, name: 'js-ts', description: 'test', detects: ['package.json'],
|
|
286
|
+
coverage: { schemaVersion: 1, classes: { format: { description: 'format', detectors: [{ sensor: 'format' }], remedy: { summary: 'Fix format', command: 'awm sensors init --pack js-ts' } } } },
|
|
287
|
+
sensors: { format: {
|
|
288
|
+
applicability: { kind: 'explicit-opt-in' },
|
|
289
|
+
variants: [{
|
|
290
|
+
id: 'prettier', priority: 10, certifiedRange: '>=3.0.0 <4.0.0',
|
|
291
|
+
requirements: { tool: 'prettier', toolRange: '>=3.0.0 <4.0.0', runtime: 'node', runtimeRange: '>=0.0.0' },
|
|
292
|
+
assets: [], formatter: 'prettier', probe: { kind: 'package-script-present', script: 'format' },
|
|
293
|
+
command: { executable: 'prettier', resolution: 'node-modules-bin', args: ['.'] },
|
|
294
|
+
}],
|
|
295
|
+
} },
|
|
296
|
+
}));
|
|
297
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({
|
|
298
|
+
devDependencies: { prettier: '^3.0.0' },
|
|
299
|
+
scripts: { format: 'prettier .' },
|
|
300
|
+
}));
|
|
301
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'prettier'), { recursive: true });
|
|
302
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'prettier', 'package.json'), JSON.stringify({ version: '3.3.3' }));
|
|
303
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', '.bin'), { recursive: true });
|
|
304
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', '.bin', 'prettier'), '#!/bin/sh\nexit 0\n');
|
|
305
|
+
fs_1.default.chmodSync(path_1.default.join(tmpDir, 'node_modules', '.bin', 'prettier'), 0o755);
|
|
306
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
307
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
308
|
+
schemaVersion: 2, pack: 'js-ts', packSelection: 'explicit',
|
|
309
|
+
sensors: { format: {
|
|
310
|
+
enabled: true, variantId: 'prettier',
|
|
311
|
+
command: { executable: 'prettier', resolution: 'node-modules-bin', args: ['.'] },
|
|
312
|
+
assets: [],
|
|
313
|
+
initializedCompatibility: {
|
|
314
|
+
state: 'certified', reason: 'range-and-probe', variantId: 'prettier',
|
|
315
|
+
toolVersion: '3.3.3', runtimeVersion: process.versions.node,
|
|
316
|
+
certifiedRange: '>=3.0.0 <4.0.0', evidence: [],
|
|
317
|
+
},
|
|
318
|
+
} },
|
|
319
|
+
}));
|
|
320
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
321
|
+
expect(result.checks.format?.detail ?? '').not.toMatch(/compatibility drift/i);
|
|
322
|
+
expect(result.checks.format).toMatchObject({ ok: true });
|
|
323
|
+
expect(result.overall).toBe('READY');
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
if (previousHome === undefined)
|
|
327
|
+
delete process.env.AWM_HOME;
|
|
328
|
+
else
|
|
329
|
+
process.env.AWM_HOME = previousHome;
|
|
330
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
271
333
|
it('marks disabled sensors as ok', async () => {
|
|
272
334
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
273
335
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const init_1 = require("../../../src/commands/watch/init");
|
|
11
|
+
const awm_gitignore_1 = require("../../../src/core/awm-gitignore");
|
|
12
|
+
const git_1 = require("../../../src/core/tracks/git");
|
|
13
|
+
describe('initWatch managed awm gitignore (GI-2/GI-10)', () => {
|
|
14
|
+
let repo;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-watch-gi-'));
|
|
17
|
+
(0, child_process_1.execFileSync)('git', ['init'], { cwd: repo, stdio: 'pipe' });
|
|
18
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'package.json'), JSON.stringify({ scripts: { test: 'jest' } }));
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
it('writes the managed block instead of a bare .awm/ allow-nothing rule', () => {
|
|
24
|
+
(0, init_1.initWatch)(repo, 'rama');
|
|
25
|
+
const gi = fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8');
|
|
26
|
+
expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
|
|
27
|
+
expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_END);
|
|
28
|
+
expect(gi).toContain('!.awm/sensors.json');
|
|
29
|
+
expect(gi).toContain('!.awm/profile.json');
|
|
30
|
+
const lines = gi.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
31
|
+
expect(lines.filter((l) => l === '.awm' || l === '.awm/')).toEqual([]);
|
|
32
|
+
expect((0, git_1.isAwmGitignored)(repo)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -301,13 +301,10 @@ describe('Supervisor.tick() — freeze de un track individual (R5.2/R6.3)', () =
|
|
|
301
301
|
}
|
|
302
302
|
function setUpGreenTrack() {
|
|
303
303
|
initCompactWatch(repo, BRANCH);
|
|
304
|
-
// `initWatch`
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
// commit, `isWorktreeClean` nunca sería `true` y el freeze jamás
|
|
309
|
-
// convergería — no por un bug del driver, sino por un repo de test
|
|
310
|
-
// que nunca terminó su propio setup.
|
|
304
|
+
// `initWatch` upserts the managed `# AWM:GITIGNORE` block but does not
|
|
305
|
+
// commit it — an untracked/modified `.gitignore` keeps the worktree
|
|
306
|
+
// dirty for R6.4's clean check. Commit the bootstrap side-effect so
|
|
307
|
+
// freeze can converge.
|
|
311
308
|
git(repo, 'add', '.gitignore');
|
|
312
309
|
git(repo, 'commit', '-qm', 'gitignore .awm');
|
|
313
310
|
const s = (0, store_1.readJournal)(repo, BRANCH).state;
|
|
@@ -526,6 +523,13 @@ describe('loop end-to-end: plan (reconcileTracks) + track (Supervisor.tick) cier
|
|
|
526
523
|
test('reconcileTracks real (plan) + Supervisor.tick real (track), en loop contra dos worktrees reales, cierran request -> drenaje -> persist -> observación -> FROZEN', async () => {
|
|
527
524
|
// --- lado TRACK: registrar el trabajo y drenarlo con un Supervisor REAL ---
|
|
528
525
|
initCompactWatch(trackWorktree, TRACK_BRANCH);
|
|
526
|
+
// `initWatch` migrates the seeded `.awm/` ignore into the managed
|
|
527
|
+
// `.awm/*` allowlist block. That (1) dirties `.gitignore` and (2)
|
|
528
|
+
// un-ignores `.awm/sensors.json`, which then shows up as untracked.
|
|
529
|
+
// Both must be committed or R6.4's clean-worktree gate never passes
|
|
530
|
+
// and this loop times out (same class of fixture bug as setUpGreenTrack).
|
|
531
|
+
git(trackWorktree, 'add', '.gitignore', '.awm/sensors.json');
|
|
532
|
+
git(trackWorktree, 'commit', '-qm', 'awm gitignore + sensors');
|
|
529
533
|
const ts0 = (0, store_1.readJournal)(trackWorktree, TRACK_BRANCH).state;
|
|
530
534
|
ts0.trackContext = { trackId: 'a', taskIds: ['T1'], planDigest: '', baseSha, planJournalId: 'j-plan' };
|
|
531
535
|
(0, store_1.writeJournal)(trackWorktree, TRACK_BRANCH, ts0);
|
|
@@ -154,7 +154,9 @@ describe('watch --init: plan-vs-repo mecanico', () => {
|
|
|
154
154
|
const out = (0, init_1.initWatch)(repo, 'rama');
|
|
155
155
|
expect(out.requiredVerifiers).toEqual(['test']);
|
|
156
156
|
expect((0, store_1.readJournal)(repo, 'rama').state.requiredVerifiers).toEqual(['test']);
|
|
157
|
-
|
|
157
|
+
const gi = fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8');
|
|
158
|
+
expect(gi).toContain('# AWM:GITIGNORE:BEGIN');
|
|
159
|
+
expect(gi).toContain('.awm/*');
|
|
158
160
|
expect(() => (0, init_1.initWatch)(repo, 'rama')).not.toThrow(); // idempotente
|
|
159
161
|
});
|
|
160
162
|
test('no escribe .gitignore cuando es un symlink', () => {
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const awm_gitignore_1 = require("../../src/core/awm-gitignore");
|
|
10
|
+
describe('awm-gitignore managed block', () => {
|
|
11
|
+
let tmp;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-gi-'));
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
it('renders the canonical managed block with versioned allowlist entries', () => {
|
|
19
|
+
expect((0, awm_gitignore_1.renderManagedBlock)()).toBe([
|
|
20
|
+
awm_gitignore_1.AWM_GITIGNORE_BEGIN,
|
|
21
|
+
'.awm/*',
|
|
22
|
+
'!.awm/sensors.json',
|
|
23
|
+
'!.awm/sensors.baseline.json',
|
|
24
|
+
'!.awm/profile.json',
|
|
25
|
+
'!.awm/context-budget.json',
|
|
26
|
+
'!.awm/context/',
|
|
27
|
+
'.awm/context/*',
|
|
28
|
+
'!.awm/context/index.json',
|
|
29
|
+
awm_gitignore_1.AWM_GITIGNORE_END,
|
|
30
|
+
'',
|
|
31
|
+
].join('\n'));
|
|
32
|
+
});
|
|
33
|
+
it('creates .gitignore with the managed block and is idempotent', () => {
|
|
34
|
+
(0, awm_gitignore_1.upsertAwmGitignore)(tmp);
|
|
35
|
+
const first = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
|
|
36
|
+
expect(first).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
|
|
37
|
+
expect(first).toContain(awm_gitignore_1.AWM_GITIGNORE_END);
|
|
38
|
+
(0, awm_gitignore_1.upsertAwmGitignore)(tmp);
|
|
39
|
+
expect(fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8')).toBe(first);
|
|
40
|
+
});
|
|
41
|
+
it('migrates bare .awm/ and .awm lines into the managed block without dropping unrelated rules', () => {
|
|
42
|
+
fs_1.default.writeFileSync(path_1.default.join(tmp, '.gitignore'), 'node_modules\n.awm/\ndist\n');
|
|
43
|
+
(0, awm_gitignore_1.upsertAwmGitignore)(tmp);
|
|
44
|
+
const text = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
|
|
45
|
+
expect(text).toContain('node_modules');
|
|
46
|
+
expect(text).toContain('dist');
|
|
47
|
+
expect(text).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
|
|
48
|
+
const outside = text.slice(0, text.indexOf(awm_gitignore_1.AWM_GITIGNORE_BEGIN));
|
|
49
|
+
expect(outside.split('\n').map((l) => l.trim()).filter(Boolean)).not.toContain('.awm/');
|
|
50
|
+
expect(outside.split('\n').map((l) => l.trim()).filter(Boolean)).not.toContain('.awm');
|
|
51
|
+
});
|
|
52
|
+
it('refuses a symlink .gitignore', () => {
|
|
53
|
+
const target = path_1.default.join(tmp, 'ignore-target');
|
|
54
|
+
fs_1.default.writeFileSync(target, 'x\n');
|
|
55
|
+
fs_1.default.symlinkSync(target, path_1.default.join(tmp, '.gitignore'));
|
|
56
|
+
expect(() => (0, awm_gitignore_1.upsertAwmGitignore)(tmp)).toThrow(/symlink/i);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const awm_gitignore_1 = require("../../../src/core/awm-gitignore");
|
|
11
|
+
const checks_1 = require("../../../src/core/diagnostics/checks");
|
|
12
|
+
function git(cwd, args) {
|
|
13
|
+
(0, child_process_1.execFileSync)('git', args, { cwd, stdio: 'pipe' });
|
|
14
|
+
}
|
|
15
|
+
const stubMachine = () => ({
|
|
16
|
+
registryCache: { present: true, gitState: 'clean' },
|
|
17
|
+
hook: { present: true, applicable: true },
|
|
18
|
+
devCore: { present: true, brokenLinks: [] },
|
|
19
|
+
ambient: { wanted: [], installed: [] },
|
|
20
|
+
contextInjection: [],
|
|
21
|
+
globalSkills: { valid: [], repairable: [], dead: [], usurped: [] },
|
|
22
|
+
});
|
|
23
|
+
const stubProject = (root) => ({
|
|
24
|
+
root,
|
|
25
|
+
profile: { present: false, extensions: [] },
|
|
26
|
+
activeBundles: { expected: [], linked: [], broken: [] },
|
|
27
|
+
orphanLinks: { repairable: [], dead: [], usurped: [] },
|
|
28
|
+
sensors: { present: true },
|
|
29
|
+
constitution: { present: true },
|
|
30
|
+
context: { present: true, file: 'AGENTS.md' },
|
|
31
|
+
});
|
|
32
|
+
describe('evaluateAwmGitignorePolicy + doctor project check', () => {
|
|
33
|
+
let repo;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-gi-doc-'));
|
|
36
|
+
git(repo, ['init']);
|
|
37
|
+
git(repo, ['config', 'user.email', 't@t']);
|
|
38
|
+
git(repo, ['config', 'user.name', 't']);
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
it('fails when local runtime is tracked and proposes git rm --cached', () => {
|
|
44
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, '.awm', 'journal'), { recursive: true });
|
|
45
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'journal', 'x.json'), '{}');
|
|
46
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), 'node_modules\n');
|
|
47
|
+
git(repo, ['add', '-A']);
|
|
48
|
+
git(repo, ['commit', '-m', 'track journal']);
|
|
49
|
+
const result = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(repo);
|
|
50
|
+
expect(result.ok).toBe(false);
|
|
51
|
+
if (result.ok)
|
|
52
|
+
return;
|
|
53
|
+
expect(result.detail).toMatch(/tracked local runtime/i);
|
|
54
|
+
expect(result.remedy).toMatch(/git rm --cached/);
|
|
55
|
+
expect(result.remedy).toMatch(/awm sensors init|awm watch init/);
|
|
56
|
+
});
|
|
57
|
+
it('fails when sensors.json exists but is ignored by bare .awm/', () => {
|
|
58
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
|
|
59
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), '{}');
|
|
60
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), '.awm/\n');
|
|
61
|
+
const result = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(repo);
|
|
62
|
+
expect(result.ok).toBe(false);
|
|
63
|
+
if (result.ok)
|
|
64
|
+
return;
|
|
65
|
+
expect(result.detail).toMatch(/versioned config ignored/i);
|
|
66
|
+
expect(result.detail).toMatch(/managed.*block missing/i);
|
|
67
|
+
expect(result.remedy).toMatch(/awm sensors init|awm watch init/);
|
|
68
|
+
});
|
|
69
|
+
it('fails when markers exist but the block body is incomplete', () => {
|
|
70
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
|
|
71
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), '{}');
|
|
72
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), '# AWM:GITIGNORE:BEGIN\n.awm/*\n# AWM:GITIGNORE:END\n');
|
|
73
|
+
const result = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(repo);
|
|
74
|
+
expect(result.ok).toBe(false);
|
|
75
|
+
if (result.ok)
|
|
76
|
+
return;
|
|
77
|
+
expect(result.detail).toMatch(/managed.*block missing|incomplete/i);
|
|
78
|
+
});
|
|
79
|
+
it('passes when the managed block is installed', () => {
|
|
80
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
|
|
81
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), '{}');
|
|
82
|
+
(0, awm_gitignore_1.upsertAwmGitignore)(repo);
|
|
83
|
+
git(repo, ['add', '.gitignore', '.awm/sensors.json']);
|
|
84
|
+
git(repo, ['commit', '-m', 'ok']);
|
|
85
|
+
expect((0, awm_gitignore_1.evaluateAwmGitignorePolicy)(repo)).toMatchObject({ ok: true });
|
|
86
|
+
});
|
|
87
|
+
it('runChecks emits project.awm-gitignore missing when policy fails', () => {
|
|
88
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
|
|
89
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), '{}');
|
|
90
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), '.awm/\n');
|
|
91
|
+
const ctx = {
|
|
92
|
+
machine: stubMachine(),
|
|
93
|
+
project: stubProject(repo),
|
|
94
|
+
providers: [],
|
|
95
|
+
};
|
|
96
|
+
const report = (0, checks_1.runChecks)(ctx);
|
|
97
|
+
const row = report.results.find((r) => r.id === 'project.awm-gitignore');
|
|
98
|
+
expect(row).toMatchObject({ status: 'missing' });
|
|
99
|
+
expect(report.overall).toBe('degraded');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -8,6 +8,11 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
test('.awm (and therefore .awm/ledger) is gitignored — raw ledger never committed', () => {
|
|
9
9
|
const gitignore = fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../../.gitignore'), 'utf-8');
|
|
10
10
|
const lines = gitignore.split('\n').map(l => l.trim());
|
|
11
|
-
|
|
11
|
+
// D-024: managed block uses `.awm/*` (with allowlist exceptions), not a bare `.awm` / `.awm/`.
|
|
12
|
+
const coversAwm = lines.includes('.awm/*') ||
|
|
13
|
+
lines.includes('.awm') ||
|
|
14
|
+
lines.includes('.awm/') ||
|
|
15
|
+
lines.includes('.awm/ledger') ||
|
|
16
|
+
lines.includes('.awm/ledger/');
|
|
12
17
|
expect(coversAwm).toBe(true);
|
|
13
18
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.21.0",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"lint": "eslint . --config eslint.config.awm.mjs --cache --format json",
|
|
27
27
|
"depcheck": "depcruise --config .dep-cruiser.awm.js src",
|
|
28
28
|
"test": "jest --runInBand",
|
|
29
|
+
"test:wait-npm-publication": "node --test ../.github/scripts/wait-npm-publication.test.cjs",
|
|
29
30
|
"start": "ts-node src/index.ts",
|
|
30
31
|
"prerelease": "npm run build",
|
|
31
32
|
"release": "node dist/src/release/index.js",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|