@ai-content-space/loopx 0.1.2 → 0.1.4
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 +422 -57
- package/README.zh-CN.md +485 -0
- package/assets/logo.svg +89 -0
- package/package.json +5 -1
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/scripts/plugin-install.test.mjs +14 -0
- package/plugins/loopx/skills/archive/SKILL.md +49 -0
- package/plugins/loopx/skills/build/SKILL.md +111 -9
- package/plugins/loopx/skills/clarify/SKILL.md +129 -8
- 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 +24 -3
- 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 +248 -0
- package/skills/archive/SKILL.md +49 -0
- package/skills/build/SKILL.md +111 -9
- package/skills/clarify/SKILL.md +129 -8
- 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 +20 -3
- 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 +311 -26
- package/src/build-stop-gate.mjs +94 -0
- package/src/cli.mjs +57 -5
- package/src/codex-exec-runtime.mjs +105 -5
- package/src/context-manifest.mjs +172 -0
- package/src/html-views.mjs +316 -0
- package/src/install-discovery.mjs +352 -5
- package/src/next-skill.mjs +57 -5
- package/src/plan-runtime.mjs +102 -122
- package/src/review-runtime.mjs +558 -0
- package/src/runtime-maintenance.mjs +429 -14
- package/src/template-governance.mjs +223 -0
- package/src/workflow.mjs +2341 -120
- package/src/workspace-context.mjs +166 -0
- package/src/workspace-memory.mjs +69 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const TEMPLATE_BASELINE_SCHEMA_VERSION = 1;
|
|
7
|
+
export const TEMPLATE_DRIFT_STATUSES = [
|
|
8
|
+
'current',
|
|
9
|
+
'outdated-pristine',
|
|
10
|
+
'user-modified',
|
|
11
|
+
'conflict',
|
|
12
|
+
'unknown',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
async function sha256File(path) {
|
|
16
|
+
const hash = createHash('sha256');
|
|
17
|
+
hash.update(await readFile(path));
|
|
18
|
+
return hash.digest('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sha256Text(text) {
|
|
22
|
+
const hash = createHash('sha256');
|
|
23
|
+
hash.update(text);
|
|
24
|
+
return hash.digest('hex');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseManagedBlocks(text) {
|
|
28
|
+
const pattern = /<!--\s*loopx:managed:block\s+([A-Za-z0-9_.:-]+)\s*-->([\s\S]*?)<!--\s*\/loopx:managed:block\s+\1\s*-->/g;
|
|
29
|
+
const blocks = [];
|
|
30
|
+
let match;
|
|
31
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
32
|
+
blocks.push({
|
|
33
|
+
id: match[1],
|
|
34
|
+
content: match[2],
|
|
35
|
+
start: match.index,
|
|
36
|
+
end: pattern.lastIndex,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return blocks;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function managedBlockSnapshot(text, managedBlockId = null) {
|
|
43
|
+
const blocks = parseManagedBlocks(text)
|
|
44
|
+
.filter((block) => !managedBlockId || block.id === managedBlockId);
|
|
45
|
+
return blocks.map((block, index) => {
|
|
46
|
+
const previousEnd = index === 0 ? 0 : blocks[index - 1].end;
|
|
47
|
+
const nextStart = index === blocks.length - 1 ? text.length : blocks[index + 1].start;
|
|
48
|
+
return {
|
|
49
|
+
id: block.id,
|
|
50
|
+
hash: sha256Text(block.content),
|
|
51
|
+
protected_user_regions: [
|
|
52
|
+
...(text.slice(previousEnd, block.start).trim() ? [`before:${block.id}`] : []),
|
|
53
|
+
...(text.slice(block.end, nextStart).trim() ? [`after:${block.id}`] : []),
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizePath(root, path) {
|
|
60
|
+
const resolved = resolve(path);
|
|
61
|
+
const rel = relative(root, resolved);
|
|
62
|
+
return rel && !rel.startsWith('..') ? rel : resolved;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function createTemplateBaseline(root, items, options = {}) {
|
|
66
|
+
const resolvedRoot = resolve(root);
|
|
67
|
+
const baselineItems = [];
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
const targetPath = resolve(item.path);
|
|
70
|
+
const sourcePath = resolve(item.sourcePath || item.path);
|
|
71
|
+
const currentHash = existsSync(targetPath) ? await sha256File(targetPath) : null;
|
|
72
|
+
const registryHash = existsSync(sourcePath) ? await sha256File(sourcePath) : currentHash;
|
|
73
|
+
const targetText = existsSync(targetPath) ? await readFile(targetPath, 'utf8') : '';
|
|
74
|
+
const sourceText = existsSync(sourcePath) ? await readFile(sourcePath, 'utf8') : targetText;
|
|
75
|
+
const managedBlockId = item.managedBlockId || null;
|
|
76
|
+
baselineItems.push({
|
|
77
|
+
path: normalizePath(resolvedRoot, targetPath),
|
|
78
|
+
source_path: normalizePath(resolvedRoot, sourcePath),
|
|
79
|
+
kind: item.kind || 'file',
|
|
80
|
+
hash: currentHash,
|
|
81
|
+
registry_hash: registryHash,
|
|
82
|
+
managed_block_id: managedBlockId,
|
|
83
|
+
managed_block_hashes: managedBlockSnapshot(targetText, managedBlockId),
|
|
84
|
+
registry_managed_block_hashes: managedBlockSnapshot(sourceText, managedBlockId),
|
|
85
|
+
installed_at: options.installedAt || new Date().toISOString(),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
schema_version: TEMPLATE_BASELINE_SCHEMA_VERSION,
|
|
90
|
+
generated_by: 'loopx',
|
|
91
|
+
registry_revision: options.registryRevision || 'local',
|
|
92
|
+
items: baselineItems,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function readTemplateBaseline(path) {
|
|
97
|
+
if (!existsSync(path)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function writeTemplateBaseline(path, baseline) {
|
|
104
|
+
await mkdir(dirname(path), { recursive: true });
|
|
105
|
+
await writeFile(path, `${JSON.stringify(baseline, null, 2)}\n`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveItemPath(itemPath, explicitPath, root) {
|
|
109
|
+
if (explicitPath) {
|
|
110
|
+
return resolve(explicitPath);
|
|
111
|
+
}
|
|
112
|
+
return root ? resolve(root, itemPath) : resolve(itemPath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function classifyTemplateDrift(item, options = {}) {
|
|
116
|
+
if (!item || item.hash === undefined || item.hash === null) {
|
|
117
|
+
return { status: 'unknown', reason: 'missing_baseline_hash' };
|
|
118
|
+
}
|
|
119
|
+
const targetPath = resolveItemPath(item.path, options.targetPath, options.root);
|
|
120
|
+
const sourcePath = resolveItemPath(item.source_path || item.path, options.sourcePath, options.root);
|
|
121
|
+
if (!existsSync(targetPath)) {
|
|
122
|
+
return { status: 'unknown', reason: 'missing_target' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const currentHash = await sha256File(targetPath);
|
|
126
|
+
const baselineHash = item.hash;
|
|
127
|
+
const registryHash = existsSync(sourcePath) ? await sha256File(sourcePath) : (item.registry_hash || baselineHash);
|
|
128
|
+
if (item.managed_block_id || (Array.isArray(item.managed_block_hashes) && item.managed_block_hashes.length > 0)) {
|
|
129
|
+
const targetText = await readFile(targetPath, 'utf8');
|
|
130
|
+
const sourceText = existsSync(sourcePath) ? await readFile(sourcePath, 'utf8') : targetText;
|
|
131
|
+
const currentBlocks = managedBlockSnapshot(targetText, item.managed_block_id);
|
|
132
|
+
const registryBlocks = managedBlockSnapshot(sourceText, item.managed_block_id);
|
|
133
|
+
const baselineBlocks = Array.isArray(item.managed_block_hashes) ? item.managed_block_hashes : [];
|
|
134
|
+
if (baselineBlocks.length === 0 || currentBlocks.length !== baselineBlocks.length) {
|
|
135
|
+
return { status: 'unknown', reason: 'missing_managed_block' };
|
|
136
|
+
}
|
|
137
|
+
const currentMatchesBaseline = currentBlocks.every((block) => baselineBlocks.some((baseline) => baseline.id === block.id && baseline.hash === block.hash));
|
|
138
|
+
const registryMatchesBaseline = registryBlocks.length === baselineBlocks.length
|
|
139
|
+
&& registryBlocks.every((block) => baselineBlocks.some((baseline) => baseline.id === block.id && baseline.hash === block.hash));
|
|
140
|
+
const currentMatchesRegistry = currentBlocks.length === registryBlocks.length
|
|
141
|
+
&& currentBlocks.every((block) => registryBlocks.some((registry) => registry.id === block.id && registry.hash === block.hash));
|
|
142
|
+
const protectedRegions = [...new Set(currentBlocks.flatMap((block) => block.protected_user_regions || []))];
|
|
143
|
+
const blockResult = {
|
|
144
|
+
currentHash,
|
|
145
|
+
registryHash,
|
|
146
|
+
baselineHash,
|
|
147
|
+
managedBlockHashes: currentBlocks,
|
|
148
|
+
protected_user_regions: protectedRegions,
|
|
149
|
+
};
|
|
150
|
+
if (currentMatchesRegistry) {
|
|
151
|
+
return { status: 'current', ...blockResult };
|
|
152
|
+
}
|
|
153
|
+
if (currentMatchesBaseline && !registryMatchesBaseline) {
|
|
154
|
+
return { status: 'outdated-pristine', ...blockResult };
|
|
155
|
+
}
|
|
156
|
+
if (registryMatchesBaseline && !currentMatchesBaseline) {
|
|
157
|
+
return { status: 'user-modified', ...blockResult };
|
|
158
|
+
}
|
|
159
|
+
return { status: 'conflict', ...blockResult };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (currentHash === registryHash) {
|
|
163
|
+
return {
|
|
164
|
+
status: 'current',
|
|
165
|
+
currentHash,
|
|
166
|
+
registryHash,
|
|
167
|
+
baselineHash,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (currentHash === baselineHash && registryHash !== baselineHash) {
|
|
171
|
+
return {
|
|
172
|
+
status: 'outdated-pristine',
|
|
173
|
+
currentHash,
|
|
174
|
+
registryHash,
|
|
175
|
+
baselineHash,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (registryHash === baselineHash && currentHash !== baselineHash) {
|
|
179
|
+
return {
|
|
180
|
+
status: 'user-modified',
|
|
181
|
+
currentHash,
|
|
182
|
+
registryHash,
|
|
183
|
+
baselineHash,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
status: 'conflict',
|
|
188
|
+
currentHash,
|
|
189
|
+
registryHash,
|
|
190
|
+
baselineHash,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function inspectTemplateGovernance(baselinePath) {
|
|
195
|
+
const baseline = await readTemplateBaseline(baselinePath);
|
|
196
|
+
if (!baseline) {
|
|
197
|
+
return {
|
|
198
|
+
schema_version: TEMPLATE_BASELINE_SCHEMA_VERSION,
|
|
199
|
+
status: 'missing',
|
|
200
|
+
baselinePath,
|
|
201
|
+
items: [],
|
|
202
|
+
summary: {},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const items = [];
|
|
206
|
+
const baselineRoot = dirname(dirname(resolve(baselinePath)));
|
|
207
|
+
for (const item of baseline.items || []) {
|
|
208
|
+
const drift = await classifyTemplateDrift(item, { root: baselineRoot });
|
|
209
|
+
items.push({ ...item, drift_status: drift.status, drift_reason: drift.reason || null });
|
|
210
|
+
}
|
|
211
|
+
const summary = {};
|
|
212
|
+
for (const item of items) {
|
|
213
|
+
summary[item.drift_status] = (summary[item.drift_status] || 0) + 1;
|
|
214
|
+
}
|
|
215
|
+
const nonCurrent = items.find((item) => item.drift_status !== 'current');
|
|
216
|
+
return {
|
|
217
|
+
schema_version: baseline.schema_version || TEMPLATE_BASELINE_SCHEMA_VERSION,
|
|
218
|
+
status: nonCurrent ? 'drift' : 'current',
|
|
219
|
+
baselinePath,
|
|
220
|
+
items,
|
|
221
|
+
summary,
|
|
222
|
+
};
|
|
223
|
+
}
|