@ai-content-space/loopx 0.1.2 → 0.1.3
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/README.md +343 -56
- package/README.zh-CN.md +392 -0
- package/package.json +4 -1
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/scripts/plugin-install.test.mjs +1 -0
- package/plugins/loopx/skills/archive/SKILL.md +39 -0
- package/plugins/loopx/skills/build/SKILL.md +111 -9
- package/plugins/loopx/skills/clarify/SKILL.md +121 -1
- package/plugins/loopx/skills/debug/SKILL.md +296 -0
- package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
- package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
- package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
- package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
- package/plugins/loopx/skills/go-style/SKILL.md +71 -0
- package/plugins/loopx/skills/kratos/SKILL.md +74 -0
- package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
- package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
- package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
- package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
- package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
- package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
- package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
- package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
- package/plugins/loopx/skills/plan/SKILL.md +22 -2
- package/plugins/loopx/skills/review/SKILL.md +98 -1
- package/plugins/loopx/skills/tdd/SKILL.md +371 -0
- package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
- package/plugins/loopx/skills/verify/SKILL.md +139 -0
- package/scripts/codex-stop-hook.mjs +71 -0
- package/scripts/codex-workflow-hook.mjs +153 -0
- package/skills/archive/SKILL.md +39 -0
- package/skills/build/SKILL.md +111 -9
- package/skills/clarify/SKILL.md +121 -1
- package/skills/debug/SKILL.md +296 -0
- package/skills/debug/condition-based-waiting.md +115 -0
- package/skills/debug/defense-in-depth.md +122 -0
- package/skills/debug/find-polluter.sh +63 -0
- package/skills/debug/root-cause-tracing.md +169 -0
- package/skills/go-style/SKILL.md +71 -0
- package/skills/kratos/SKILL.md +74 -0
- package/skills/kratos/references/advanced-features.md +314 -0
- package/skills/kratos/references/architecture.md +488 -0
- package/skills/kratos/references/configuration.md +399 -0
- package/skills/kratos/references/http-customization.md +512 -0
- package/skills/kratos/references/middleware-logging.md +400 -0
- package/skills/kratos/references/proto-api-design.md +432 -0
- package/skills/kratos/references/security-auth.md +411 -0
- package/skills/kratos/references/troubleshooting.md +385 -0
- package/skills/plan/SKILL.md +18 -2
- package/skills/review/SKILL.md +98 -1
- package/skills/tdd/SKILL.md +371 -0
- package/skills/tdd/testing-anti-patterns.md +299 -0
- package/skills/verify/SKILL.md +139 -0
- package/src/build-runtime.mjs +303 -26
- package/src/build-stop-gate.mjs +94 -0
- package/src/cli.mjs +47 -5
- package/src/codex-exec-runtime.mjs +105 -5
- package/src/context-manifest.mjs +172 -0
- package/src/install-discovery.mjs +352 -5
- package/src/next-skill.mjs +57 -5
- package/src/plan-runtime.mjs +79 -122
- package/src/review-runtime.mjs +378 -0
- package/src/runtime-maintenance.mjs +428 -14
- package/src/template-governance.mjs +223 -0
- package/src/workflow.mjs +1941 -117
- package/src/workspace-context.mjs +166 -0
- package/src/workspace-memory.mjs +69 -0
|
@@ -4,6 +4,14 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
classifyTemplateDrift,
|
|
9
|
+
createTemplateBaseline,
|
|
10
|
+
inspectTemplateGovernance,
|
|
11
|
+
readTemplateBaseline,
|
|
12
|
+
writeTemplateBaseline,
|
|
13
|
+
} from './template-governance.mjs';
|
|
14
|
+
|
|
7
15
|
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
8
16
|
const PROJECT_ROOT = resolve(MODULE_DIR, '..');
|
|
9
17
|
const LOOPX_SKILLS = [
|
|
@@ -12,8 +20,69 @@ const LOOPX_SKILLS = [
|
|
|
12
20
|
'build',
|
|
13
21
|
'review',
|
|
14
22
|
'autopilot',
|
|
23
|
+
'archive',
|
|
24
|
+
'debug',
|
|
25
|
+
'tdd',
|
|
26
|
+
'verify',
|
|
27
|
+
'go-style',
|
|
28
|
+
'kratos',
|
|
15
29
|
];
|
|
16
30
|
const LOOPX_INSTALLATION_IDENTITY = 'loopx';
|
|
31
|
+
const LOOPX_MANAGED_SCRIPT_ITEMS = [
|
|
32
|
+
{
|
|
33
|
+
name: 'codex-workflow-hook',
|
|
34
|
+
kind: 'hook',
|
|
35
|
+
sourceRelativePath: 'scripts/codex-workflow-hook.mjs',
|
|
36
|
+
targetRelativePath: '.codex/hooks/codex-workflow-hook.mjs',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
const LOOPX_GOVERNED_SOURCE_ITEMS = [
|
|
40
|
+
{
|
|
41
|
+
name: 'loopx-plugin-manifest',
|
|
42
|
+
kind: 'plugin',
|
|
43
|
+
sourceRelativePath: 'plugins/loopx/.codex-plugin/plugin.json',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'loopx-plugin-install-script',
|
|
47
|
+
kind: 'plugin',
|
|
48
|
+
sourceRelativePath: 'plugins/loopx/scripts/plugin-install.mjs',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'workflow-template-architecture',
|
|
52
|
+
kind: 'workflow-template',
|
|
53
|
+
sourceRelativePath: 'templates/architecture.md',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'workflow-template-development-plan',
|
|
57
|
+
kind: 'workflow-template',
|
|
58
|
+
sourceRelativePath: 'templates/development-plan.md',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'workflow-template-execution-record',
|
|
62
|
+
kind: 'workflow-template',
|
|
63
|
+
sourceRelativePath: 'templates/execution-record.md',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'workflow-template-plan',
|
|
67
|
+
kind: 'workflow-template',
|
|
68
|
+
sourceRelativePath: 'templates/plan.md',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'workflow-template-review-report',
|
|
72
|
+
kind: 'workflow-template',
|
|
73
|
+
sourceRelativePath: 'templates/review-report.md',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'workflow-template-spec',
|
|
77
|
+
kind: 'workflow-template',
|
|
78
|
+
sourceRelativePath: 'templates/spec.md',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'workflow-template-test-plan',
|
|
82
|
+
kind: 'workflow-template',
|
|
83
|
+
sourceRelativePath: 'templates/test-plan.md',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
17
86
|
|
|
18
87
|
function jsonClone(value) {
|
|
19
88
|
return JSON.parse(JSON.stringify(value));
|
|
@@ -44,12 +113,18 @@ export function getSkillSourceRoot(env = process.env) {
|
|
|
44
113
|
return resolve(env.LOOPX_SKILL_SOURCE_ROOT || join(getProjectRoot(env), 'skills'));
|
|
45
114
|
}
|
|
46
115
|
|
|
116
|
+
export function getTemplateBaselinePath(env = process.env) {
|
|
117
|
+
const home = resolve(env.LOOPX_HOME || env.HOME || process.cwd());
|
|
118
|
+
return resolve(env.LOOPX_TEMPLATE_BASELINE_PATH || join(home, '.loopx', 'template-hashes.json'));
|
|
119
|
+
}
|
|
120
|
+
|
|
47
121
|
function getInstallOptions(options = {}, env = process.env) {
|
|
48
122
|
return {
|
|
49
123
|
installationIdentity: options.installationIdentity || env.LOOPX_INSTALLATION_IDENTITY || LOOPX_INSTALLATION_IDENTITY,
|
|
50
124
|
distributionChannel: options.distributionChannel || env.LOOPX_DISTRIBUTION_CHANNEL || 'npm',
|
|
51
125
|
sourceUrl: resolve(options.sourceUrl || env.LOOPX_SOURCE_URL || getProjectRoot(env)),
|
|
52
126
|
skillSourceRoot: resolve(options.skillSourceRoot || getSkillSourceRoot(env)),
|
|
127
|
+
installMethod: options.installMethod || env.LOOPX_INSTALL_METHOD || 'copy',
|
|
53
128
|
};
|
|
54
129
|
}
|
|
55
130
|
|
|
@@ -57,6 +132,14 @@ function skillSourceDir(skillName, env = process.env, skillSourceRoot = getSkill
|
|
|
57
132
|
return join(skillSourceRoot, skillName);
|
|
58
133
|
}
|
|
59
134
|
|
|
135
|
+
function projectSourceEntry(relativePath, env = process.env) {
|
|
136
|
+
const configuredPath = join(getProjectRoot(env), relativePath);
|
|
137
|
+
if (existsSync(configuredPath)) {
|
|
138
|
+
return configuredPath;
|
|
139
|
+
}
|
|
140
|
+
return join(PROJECT_ROOT, relativePath);
|
|
141
|
+
}
|
|
142
|
+
|
|
60
143
|
function skillSourceEntry(skillName, env = process.env, skillSourceRoot = getSkillSourceRoot(env)) {
|
|
61
144
|
return join(skillSourceDir(skillName, env, skillSourceRoot), 'SKILL.md');
|
|
62
145
|
}
|
|
@@ -65,6 +148,10 @@ function installedSkillDir(skillName, env = process.env) {
|
|
|
65
148
|
return join(getInstalledSkillsRoot(env), skillName);
|
|
66
149
|
}
|
|
67
150
|
|
|
151
|
+
function installedManagedScriptPath(item, env = process.env) {
|
|
152
|
+
return join(installTemplateRoot(env), item.targetRelativePath);
|
|
153
|
+
}
|
|
154
|
+
|
|
68
155
|
async function fileHash(path) {
|
|
69
156
|
const hash = createHash('sha1');
|
|
70
157
|
const stat = await lstat(path);
|
|
@@ -131,11 +218,15 @@ async function materializeSkill(skillName, env = process.env, options = {}) {
|
|
|
131
218
|
await ensureDir(dirname(targetDir));
|
|
132
219
|
await removeInstalledSkill(targetDir);
|
|
133
220
|
|
|
134
|
-
let installMethod = 'symlink';
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
221
|
+
let installMethod = options.installMethod === 'symlink' ? 'symlink' : 'copy';
|
|
222
|
+
if (installMethod === 'symlink') {
|
|
223
|
+
try {
|
|
224
|
+
await symlink(sourceDir, targetDir, 'dir');
|
|
225
|
+
} catch {
|
|
226
|
+
installMethod = 'copy';
|
|
227
|
+
await cp(sourceDir, targetDir, { recursive: true });
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
139
230
|
await cp(sourceDir, targetDir, { recursive: true });
|
|
140
231
|
}
|
|
141
232
|
|
|
@@ -148,6 +239,119 @@ async function materializeSkill(skillName, env = process.env, options = {}) {
|
|
|
148
239
|
};
|
|
149
240
|
}
|
|
150
241
|
|
|
242
|
+
function installTemplateRoot(env = process.env) {
|
|
243
|
+
return resolve(env.LOOPX_HOME || env.HOME || process.cwd());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function templateItemKey(item) {
|
|
247
|
+
return `${item.kind || 'file'}:${item.path}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function skillTemplatePaths(skillName, env = process.env, options = {}) {
|
|
251
|
+
return {
|
|
252
|
+
targetPath: skillSourceEntry(skillName, env, getInstalledSkillsRoot(env)),
|
|
253
|
+
sourcePath: skillSourceEntry(skillName, env, options.skillSourceRoot),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function managedScriptTemplatePaths(item, env = process.env) {
|
|
258
|
+
return {
|
|
259
|
+
targetPath: installedManagedScriptPath(item, env),
|
|
260
|
+
sourcePath: projectSourceEntry(item.sourceRelativePath, env),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function governedSourceTemplatePaths(item, env = process.env) {
|
|
265
|
+
const sourcePath = projectSourceEntry(item.sourceRelativePath, env);
|
|
266
|
+
return {
|
|
267
|
+
targetPath: sourcePath,
|
|
268
|
+
sourcePath,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function createSkillTemplateItem(skillName, env = process.env, options = {}) {
|
|
273
|
+
const { targetPath, sourcePath } = skillTemplatePaths(skillName, env, options);
|
|
274
|
+
const baseline = await createTemplateBaseline(installTemplateRoot(env), [{
|
|
275
|
+
path: targetPath,
|
|
276
|
+
sourcePath,
|
|
277
|
+
kind: 'skill',
|
|
278
|
+
}], {
|
|
279
|
+
registryRevision: options.sourceUrl || 'local',
|
|
280
|
+
});
|
|
281
|
+
return baseline.items[0];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function createManagedScriptTemplateItem(item, env = process.env) {
|
|
285
|
+
const { targetPath, sourcePath } = managedScriptTemplatePaths(item, env);
|
|
286
|
+
if (!existsSync(sourcePath)) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const baseline = await createTemplateBaseline(installTemplateRoot(env), [{
|
|
290
|
+
path: targetPath,
|
|
291
|
+
sourcePath,
|
|
292
|
+
kind: item.kind || 'file',
|
|
293
|
+
}], {
|
|
294
|
+
registryRevision: item.sourceRelativePath,
|
|
295
|
+
});
|
|
296
|
+
return baseline.items[0];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function createGovernedSourceTemplateItem(item, env = process.env, options = {}) {
|
|
300
|
+
const { targetPath, sourcePath } = governedSourceTemplatePaths(item, env);
|
|
301
|
+
if (!existsSync(sourcePath)) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const baseline = await createTemplateBaseline(installTemplateRoot(env), [{
|
|
305
|
+
path: targetPath,
|
|
306
|
+
sourcePath,
|
|
307
|
+
kind: item.kind || 'file',
|
|
308
|
+
}], {
|
|
309
|
+
registryRevision: options.sourceUrl || item.sourceRelativePath,
|
|
310
|
+
});
|
|
311
|
+
return baseline.items[0];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function templateGovernanceBeforeInstall(skillName, baselineItemsByPath, env = process.env, options = {}) {
|
|
315
|
+
const { targetPath, sourcePath } = skillTemplatePaths(skillName, env, options);
|
|
316
|
+
const probe = await createSkillTemplateItem(skillName, env, options);
|
|
317
|
+
const existing = baselineItemsByPath.get(templateItemKey(probe));
|
|
318
|
+
if (!existing) {
|
|
319
|
+
return { action: 'install', drift: { status: 'unknown', reason: 'missing_baseline_item' }, item: null };
|
|
320
|
+
}
|
|
321
|
+
const drift = await classifyTemplateDrift(existing, {
|
|
322
|
+
root: installTemplateRoot(env),
|
|
323
|
+
targetPath,
|
|
324
|
+
sourcePath,
|
|
325
|
+
});
|
|
326
|
+
if (drift.status === 'user-modified' || drift.status === 'conflict') {
|
|
327
|
+
return { action: 'skip-user-modified', drift, item: existing };
|
|
328
|
+
}
|
|
329
|
+
return { action: 'install', drift, item: existing };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function mergedSkippedTemplateItem(skillName, existing, env = process.env, options = {}) {
|
|
333
|
+
const latest = await createSkillTemplateItem(skillName, env, options);
|
|
334
|
+
return {
|
|
335
|
+
...existing,
|
|
336
|
+
source_path: latest.source_path,
|
|
337
|
+
registry_hash: latest.registry_hash,
|
|
338
|
+
registry_managed_block_hashes: latest.registry_managed_block_hashes,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function mergedSkippedManagedScriptItem(item, existing, env = process.env) {
|
|
343
|
+
const latest = await createManagedScriptTemplateItem(item, env);
|
|
344
|
+
if (!latest) {
|
|
345
|
+
return existing;
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
...existing,
|
|
349
|
+
source_path: latest.source_path,
|
|
350
|
+
registry_hash: latest.registry_hash,
|
|
351
|
+
registry_managed_block_hashes: latest.registry_managed_block_hashes,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
151
355
|
function buildRegistryRow(record, env = process.env, options = {}) {
|
|
152
356
|
return {
|
|
153
357
|
source: 'loopx',
|
|
@@ -199,6 +403,18 @@ async function removeStaleOwnedInstall(currentRow) {
|
|
|
199
403
|
await removeInstalledSkill(currentRow.installedPath);
|
|
200
404
|
}
|
|
201
405
|
|
|
406
|
+
async function removeInstalledFile(path) {
|
|
407
|
+
if (!existsSync(path)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const stat = await lstat(path);
|
|
411
|
+
if (stat.isSymbolicLink()) {
|
|
412
|
+
await unlink(path);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
await rm(path, { force: true });
|
|
416
|
+
}
|
|
417
|
+
|
|
202
418
|
async function canonicalTargetOwnership(skillName, env = process.env, options = {}) {
|
|
203
419
|
const targetDir = installedSkillDir(skillName, env);
|
|
204
420
|
const sourceDir = skillSourceDir(skillName, env, options.skillSourceRoot);
|
|
@@ -226,6 +442,31 @@ async function canonicalTargetOwnership(skillName, env = process.env, options =
|
|
|
226
442
|
return { exists: true, owned: false };
|
|
227
443
|
}
|
|
228
444
|
|
|
445
|
+
async function canonicalFileOwnership(targetPath, sourcePath) {
|
|
446
|
+
if (!existsSync(targetPath)) {
|
|
447
|
+
return { exists: false, owned: false };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const stat = await lstat(targetPath);
|
|
451
|
+
if (stat.isSymbolicLink()) {
|
|
452
|
+
const linkTarget = await readlink(targetPath);
|
|
453
|
+
const resolvedLink = resolve(dirname(targetPath), linkTarget);
|
|
454
|
+
return {
|
|
455
|
+
exists: true,
|
|
456
|
+
owned: resolvedLink === sourcePath,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (stat.isFile()) {
|
|
461
|
+
return {
|
|
462
|
+
exists: true,
|
|
463
|
+
owned: await fileHash(targetPath) === await fileHash(sourcePath),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return { exists: true, owned: false };
|
|
468
|
+
}
|
|
469
|
+
|
|
229
470
|
async function assertLoopxOwnedTarget(skillName, currentRow, env = process.env, options = {}) {
|
|
230
471
|
const targetDir = installedSkillDir(skillName, env);
|
|
231
472
|
const dirExists = existsSync(targetDir);
|
|
@@ -283,11 +524,40 @@ export async function inspectInstallState(env = process.env) {
|
|
|
283
524
|
};
|
|
284
525
|
}
|
|
285
526
|
|
|
527
|
+
const managedArtifacts = {};
|
|
528
|
+
for (const item of LOOPX_MANAGED_SCRIPT_ITEMS) {
|
|
529
|
+
const targetPath = installedManagedScriptPath(item, env);
|
|
530
|
+
const sourcePath = projectSourceEntry(item.sourceRelativePath, env);
|
|
531
|
+
if (!existsSync(sourcePath)) {
|
|
532
|
+
managedArtifacts[item.name] = {
|
|
533
|
+
kind: item.kind,
|
|
534
|
+
targetPath,
|
|
535
|
+
sourcePath,
|
|
536
|
+
installed: existsSync(targetPath),
|
|
537
|
+
discovered: false,
|
|
538
|
+
loopxOwned: false,
|
|
539
|
+
available: false,
|
|
540
|
+
};
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const ownership = await canonicalFileOwnership(targetPath, sourcePath);
|
|
544
|
+
managedArtifacts[item.name] = {
|
|
545
|
+
kind: item.kind,
|
|
546
|
+
targetPath,
|
|
547
|
+
sourcePath,
|
|
548
|
+
installed: ownership.exists,
|
|
549
|
+
discovered: ownership.exists && ownership.owned,
|
|
550
|
+
loopxOwned: ownership.owned,
|
|
551
|
+
available: true,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
286
555
|
return {
|
|
287
556
|
projectRoot: getProjectRoot(env),
|
|
288
557
|
installedSkillsRoot: installedRoot,
|
|
289
558
|
skillLockPath: getSkillLockPath(env),
|
|
290
559
|
skills: bySkill,
|
|
560
|
+
managedArtifacts,
|
|
291
561
|
};
|
|
292
562
|
}
|
|
293
563
|
|
|
@@ -308,6 +578,19 @@ export async function verifyInstallState(env = process.env) {
|
|
|
308
578
|
}
|
|
309
579
|
}
|
|
310
580
|
|
|
581
|
+
for (const item of LOOPX_MANAGED_SCRIPT_ITEMS) {
|
|
582
|
+
const info = inspection.managedArtifacts?.[item.name];
|
|
583
|
+
if (!info?.available) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (!info?.installed) {
|
|
587
|
+
failures.push(`missing_managed_artifact:${item.name}`);
|
|
588
|
+
}
|
|
589
|
+
if (!info?.discovered) {
|
|
590
|
+
failures.push(`managed_artifact_unowned:${item.name}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
311
594
|
return {
|
|
312
595
|
ok: failures.length === 0,
|
|
313
596
|
failures,
|
|
@@ -321,9 +604,14 @@ export async function installBundledSkills(env = process.env, options = {}) {
|
|
|
321
604
|
const nextData = jsonClone(data);
|
|
322
605
|
nextData.version = nextData.version || 3;
|
|
323
606
|
nextData.skills = nextData.skills || {};
|
|
607
|
+
const baselinePath = getTemplateBaselinePath(env);
|
|
608
|
+
const existingBaseline = await readTemplateBaseline(baselinePath);
|
|
609
|
+
const baselineItemsByPath = new Map((existingBaseline?.items || []).map((item) => [templateItemKey(item), item]));
|
|
324
610
|
|
|
325
611
|
const installed = [];
|
|
326
612
|
const conflicts = [];
|
|
613
|
+
const skipped = [];
|
|
614
|
+
const nextTemplateItems = [];
|
|
327
615
|
for (const skillName of LOOPX_SKILLS) {
|
|
328
616
|
const current = nextData.skills[skillName];
|
|
329
617
|
const ownership = await assertLoopxOwnedTarget(skillName, current, env, installOptions);
|
|
@@ -335,6 +623,16 @@ export async function installBundledSkills(env = process.env, options = {}) {
|
|
|
335
623
|
});
|
|
336
624
|
continue;
|
|
337
625
|
}
|
|
626
|
+
const governance = await templateGovernanceBeforeInstall(skillName, baselineItemsByPath, env, installOptions);
|
|
627
|
+
if (governance.action === 'skip-user-modified') {
|
|
628
|
+
skipped.push({
|
|
629
|
+
skillName,
|
|
630
|
+
reason: governance.drift.status,
|
|
631
|
+
installedPath: ownership.targetDir,
|
|
632
|
+
});
|
|
633
|
+
nextTemplateItems.push(await mergedSkippedTemplateItem(skillName, governance.item, env, installOptions));
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
338
636
|
const record = await materializeSkill(skillName, env, installOptions);
|
|
339
637
|
const row = buildRegistryRow(record, env, installOptions);
|
|
340
638
|
if (current?.installedAt) {
|
|
@@ -348,14 +646,63 @@ export async function installBundledSkills(env = process.env, options = {}) {
|
|
|
348
646
|
row.provenance = mergedProvenance;
|
|
349
647
|
}
|
|
350
648
|
nextData.skills[skillName] = row;
|
|
649
|
+
nextTemplateItems.push(await createSkillTemplateItem(skillName, env, installOptions));
|
|
351
650
|
installed.push(row);
|
|
352
651
|
}
|
|
353
652
|
|
|
653
|
+
for (const item of LOOPX_MANAGED_SCRIPT_ITEMS) {
|
|
654
|
+
const { targetPath, sourcePath } = managedScriptTemplatePaths(item, env);
|
|
655
|
+
if (!existsSync(sourcePath)) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const probe = await createManagedScriptTemplateItem(item, env);
|
|
659
|
+
if (!probe) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const existing = baselineItemsByPath.get(templateItemKey(probe));
|
|
663
|
+
if (existing) {
|
|
664
|
+
const drift = await classifyTemplateDrift(existing, {
|
|
665
|
+
root: installTemplateRoot(env),
|
|
666
|
+
targetPath,
|
|
667
|
+
sourcePath,
|
|
668
|
+
});
|
|
669
|
+
if (drift.status === 'user-modified' || drift.status === 'conflict') {
|
|
670
|
+
skipped.push({
|
|
671
|
+
skillName: item.name,
|
|
672
|
+
reason: drift.status,
|
|
673
|
+
installedPath: targetPath,
|
|
674
|
+
});
|
|
675
|
+
nextTemplateItems.push(await mergedSkippedManagedScriptItem(item, existing, env));
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
await ensureDir(dirname(targetPath));
|
|
680
|
+
await removeInstalledFile(targetPath);
|
|
681
|
+
await cp(sourcePath, targetPath);
|
|
682
|
+
nextTemplateItems.push(await createManagedScriptTemplateItem(item, env));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
for (const item of LOOPX_GOVERNED_SOURCE_ITEMS) {
|
|
686
|
+
const templateItem = await createGovernedSourceTemplateItem(item, env, installOptions);
|
|
687
|
+
if (templateItem) {
|
|
688
|
+
nextTemplateItems.push(templateItem);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
354
692
|
await writeSkillLock(nextData, env);
|
|
693
|
+
await writeTemplateBaseline(baselinePath, {
|
|
694
|
+
schema_version: 1,
|
|
695
|
+
generated_by: 'loopx',
|
|
696
|
+
registry_revision: installOptions.sourceUrl || 'local',
|
|
697
|
+
items: nextTemplateItems,
|
|
698
|
+
});
|
|
699
|
+
const templateGovernance = await inspectTemplateGovernance(baselinePath);
|
|
355
700
|
return {
|
|
356
701
|
ok: conflicts.length === 0,
|
|
357
702
|
installed,
|
|
358
703
|
conflicts,
|
|
704
|
+
skipped,
|
|
705
|
+
templateGovernance,
|
|
359
706
|
inspection: await inspectInstallState(env),
|
|
360
707
|
};
|
|
361
708
|
}
|
package/src/next-skill.mjs
CHANGED
|
@@ -1,15 +1,67 @@
|
|
|
1
1
|
export function nextSkillCommand(state) {
|
|
2
|
-
if (!state ||
|
|
2
|
+
if (!state || !state.slug) {
|
|
3
3
|
return null;
|
|
4
4
|
}
|
|
5
|
-
|
|
5
|
+
const reviewBuildCommand = `$build --from-review .loopx/workflows/${state.slug}/review-report.md`;
|
|
6
|
+
if (state.current_stage === 'clarify'
|
|
7
|
+
&& state.clarify_current_round > 0
|
|
8
|
+
&& state.unresolved_ambiguity_count === 0
|
|
9
|
+
&& state.clarify_non_goals_resolved === true
|
|
10
|
+
&& state.clarify_decision_boundaries_resolved === true
|
|
11
|
+
&& state.clarify_pressure_pass_complete === true
|
|
12
|
+
&& typeof state.clarify_ambiguity_score === 'number'
|
|
13
|
+
&& typeof state.clarify_target_ambiguity_threshold === 'number'
|
|
14
|
+
&& state.clarify_ambiguity_score <= state.clarify_target_ambiguity_threshold) {
|
|
6
15
|
return `$plan ${state.slug}`;
|
|
7
16
|
}
|
|
17
|
+
if (state.current_stage === 'done'
|
|
18
|
+
&& state.completion_confirmed === true
|
|
19
|
+
&& state.archive_status !== 'archived') {
|
|
20
|
+
return `$archive ${state.slug}`;
|
|
21
|
+
}
|
|
22
|
+
if (state.stage_status !== 'awaiting-approval') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
8
25
|
if (state.current_stage === 'plan' && Array.isArray(state.plan_blockers) && state.plan_blockers.length === 0) {
|
|
9
|
-
return `$build
|
|
26
|
+
return `$build .loopx/plans/prd-${state.slug}.md`;
|
|
27
|
+
}
|
|
28
|
+
if (state.current_stage === 'build'
|
|
29
|
+
&& state.stage_status === 'awaiting-approval'
|
|
30
|
+
&& state.pending_user_decision === 'build->review'
|
|
31
|
+
&& state.review_status === 'ready-for-review'
|
|
32
|
+
&& state.execution_record_status === 'complete'
|
|
33
|
+
&& Array.isArray(state.build_blockers)
|
|
34
|
+
&& state.build_blockers.length === 0) {
|
|
35
|
+
return `$review .loopx/workflows/${state.slug}/execution-record.md`;
|
|
36
|
+
}
|
|
37
|
+
if (state.current_stage === 'review'
|
|
38
|
+
&& state.review_verdict === 'request-changes'
|
|
39
|
+
&& state.rollback_target === 'build'
|
|
40
|
+
&& (
|
|
41
|
+
state.pending_user_decision === 'review->build'
|
|
42
|
+
|| state.requested_transition === 'review->build'
|
|
43
|
+
|| state.approval?.build === 'requested'
|
|
44
|
+
|| state.approval?.build === 'approved'
|
|
45
|
+
)) {
|
|
46
|
+
return reviewBuildCommand;
|
|
47
|
+
}
|
|
48
|
+
if (state.current_stage === 'review'
|
|
49
|
+
&& state.review_verdict === 'request-changes'
|
|
50
|
+
&& state.requested_transition === 'review->build'
|
|
51
|
+
&& state.approval?.build === 'approved') {
|
|
52
|
+
return reviewBuildCommand;
|
|
53
|
+
}
|
|
54
|
+
if (state.current_stage === 'review'
|
|
55
|
+
&& state.review_verdict === 'request-changes'
|
|
56
|
+
&& state.requested_transition === 'review->plan'
|
|
57
|
+
&& state.approval?.rollback === 'approved') {
|
|
58
|
+
return `$plan ${state.slug}`;
|
|
10
59
|
}
|
|
11
|
-
if (state.current_stage === '
|
|
12
|
-
|
|
60
|
+
if (state.current_stage === 'review'
|
|
61
|
+
&& state.review_verdict === 'request-changes'
|
|
62
|
+
&& state.requested_transition === 'review->clarify'
|
|
63
|
+
&& state.approval?.rollback === 'approved') {
|
|
64
|
+
return `$clarify ${state.slug}`;
|
|
13
65
|
}
|
|
14
66
|
return null;
|
|
15
67
|
}
|