@ai-content-space/loopx 0.2.7 → 0.2.9
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 +21 -8
- package/README.zh-CN.md +22 -9
- package/docs/loopx/design/finish/345/255/246/344/271/240/345/256/241/350/256/241/351/234/200/346/261/202/350/256/276/350/256/241/346/226/207/346/241/243.md +1 -1
- package/docs/loopx/design/loopx-skill-suite-v1-design.md +4 -4
- package/docs/loopx/plans/2026-06-14-loopx-spec-memory-context-loading.md +948 -0
- package/docs/loopx/plans/loopx-skill-suite-v1-implementation.md +1 -1
- package/package.json +2 -2
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/skills/clarify/SKILL.md +14 -3
- package/plugins/loopx/skills/debug/SKILL.md +1 -1
- package/plugins/loopx/skills/doc-readability/SKILL.md +1 -1
- package/plugins/loopx/skills/exec/SKILL.md +2 -2
- package/plugins/loopx/skills/final-review/SKILL.md +1 -1
- package/plugins/loopx/skills/finish/SKILL.md +1 -1
- package/plugins/loopx/skills/fix-review/SKILL.md +1 -1
- package/plugins/loopx/skills/go-style/SKILL.md +1 -1
- package/plugins/loopx/skills/kratos/SKILL.md +1 -1
- package/plugins/loopx/skills/{plan → plan-to-exec}/SKILL.md +16 -5
- package/plugins/loopx/skills/refactor-plan/SKILL.md +1 -1
- package/plugins/loopx/skills/review/SKILL.md +1 -1
- package/plugins/loopx/skills/spec/DESIGN_SPEC_TEMPLATE.md +2 -2
- package/plugins/loopx/skills/spec/SKILL.md +15 -4
- package/plugins/loopx/skills/subagent-exec/SKILL.md +2 -2
- package/plugins/loopx/skills/tdd/SKILL.md +1 -1
- package/plugins/loopx/skills/verify/SKILL.md +1 -1
- package/scripts/claude-workflow-hook.mjs +2 -2
- package/scripts/codex-workflow-hook.mjs +4 -4
- package/skills/RESOLVER.md +4 -4
- package/skills/clarify/SKILL.md +14 -3
- package/skills/debug/SKILL.md +1 -1
- package/skills/doc-readability/SKILL.md +1 -1
- package/skills/exec/SKILL.md +2 -2
- package/skills/final-review/SKILL.md +1 -1
- package/skills/finish/SKILL.md +1 -1
- package/skills/fix-review/SKILL.md +1 -1
- package/skills/go-style/SKILL.md +1 -1
- package/skills/kratos/SKILL.md +1 -1
- package/skills/{plan → plan-to-exec}/SKILL.md +16 -5
- package/skills/refactor-plan/SKILL.md +1 -1
- package/skills/review/SKILL.md +1 -1
- package/skills/spec/DESIGN_SPEC_TEMPLATE.md +2 -2
- package/skills/spec/SKILL.md +15 -4
- package/skills/subagent-exec/SKILL.md +2 -2
- package/skills/tdd/SKILL.md +1 -1
- package/skills/verify/SKILL.md +1 -1
- package/src/cli.mjs +7 -4
- package/src/context-manifest.mjs +51 -1
- package/src/install-discovery.mjs +110 -1
- package/src/loopx-context-artifacts.mjs +114 -0
- package/src/next-skill.mjs +2 -2
- package/src/project-discovery.mjs +1 -0
- package/src/workflow.mjs +51 -7
package/skills/spec/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: spec
|
|
|
3
3
|
description: "Writes software design specs from already-clarified requirements, including solution approach, architecture outline, detailed design, tradeoffs, verification design, and handoff context. Not for unresolved requirements, PRD generation, implementation task planning, or code changes."
|
|
4
4
|
when_to_use: "spec, design spec, technical design, design proposal, detailed design, architecture design, 设计方案, 概要设计, 详细设计, 技术方案"
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.2.
|
|
6
|
+
version: "0.2.9"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# loopx Spec
|
|
@@ -12,6 +12,17 @@ Turn clarified requirements into design documents. Do not invent missing require
|
|
|
12
12
|
|
|
13
13
|
## Inputs
|
|
14
14
|
|
|
15
|
+
## Repo Specs And Memory Context
|
|
16
|
+
|
|
17
|
+
Before using this skill in a repository, inspect loopx long-lived context when it exists:
|
|
18
|
+
|
|
19
|
+
- If `docs/loopx/specs/` exists, inspect the directory names and filenames. If `docs/loopx/specs/index.md` exists, use it as a map, but do not require it. Read only specs relevant to the requested domain, affected files, workflow behavior, or named source document.
|
|
20
|
+
- If `.loopx/memory/MEMORY.md` exists, read it as curated project memory before deciding what is already known.
|
|
21
|
+
- If `.loopx/memory/index.jsonl` exists, use it only as a retrieval index for relevant active memory cards; do not treat it as an append-only log.
|
|
22
|
+
- Treat current user instructions and the named source document as highest priority, `docs/loopx/specs/` as binding long-lived repo rules, and `.loopx/memory/` as advisory context. Memory is advisory and must not override current task instructions, approved source docs, or repo specs.
|
|
23
|
+
|
|
24
|
+
Do not read every file under `docs/loopx/specs/` by default. Prefer relevant specs selected by filename, title, frontmatter such as `applies_to`, or the files/domains involved in the task.
|
|
25
|
+
|
|
15
26
|
Use the user's PRD, external requirements document, or approved `clarify` output as the source of truth.
|
|
16
27
|
|
|
17
28
|
Before writing, inspect relevant code and docs when the task touches an existing system. If a design question can be answered from the repo, answer it from evidence. If a material requirement, constraint, owner decision, or product behavior is still unclear, stop and route back to `clarify`.
|
|
@@ -63,14 +74,14 @@ The Markdown spec must include these sections:
|
|
|
63
74
|
- `十、排期与规划`
|
|
64
75
|
- `十一、QA`
|
|
65
76
|
|
|
66
|
-
The `十、排期与规划` section must include a `Planning Handoff` subsection stating what `plan` may decide without re-opening design and what must return to `clarify` or `spec`.
|
|
77
|
+
The `十、排期与规划` section must include a `Planning Handoff` subsection stating what `plan-to-exec` may decide without re-opening design and what must return to `clarify` or `spec`.
|
|
67
78
|
|
|
68
79
|
## Handoff
|
|
69
80
|
|
|
70
81
|
After the spec is complete, recommend:
|
|
71
82
|
|
|
72
83
|
```text
|
|
73
|
-
$plan docs/loopx/design/<需求名>需求设计文档.md
|
|
84
|
+
$plan-to-exec docs/loopx/design/<需求名>需求设计文档.md
|
|
74
85
|
```
|
|
75
86
|
|
|
76
|
-
Use `plan` only after the design document is internally consistent and all material requirements questions are resolved.
|
|
87
|
+
Use `plan-to-exec` only after the design document is internally consistent and all material requirements questions are resolved.
|
|
@@ -3,7 +3,7 @@ name: subagent-exec
|
|
|
3
3
|
description: "Executes approved loopx implementation plans with fresh subagents per independent task and staged review. Not for planning, unclear requirements, or tightly coupled edits."
|
|
4
4
|
when_to_use: "approved implementation plan, independent tasks, subagent execution, staged spec review, code quality review, parallel-capable execution"
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.2.
|
|
6
|
+
version: "0.2.9"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Subagent Exec
|
|
@@ -283,7 +283,7 @@ Done!
|
|
|
283
283
|
|
|
284
284
|
**Required workflow skills:**
|
|
285
285
|
|
|
286
|
-
- **loopx:plan** - Creates the plan this skill executes
|
|
286
|
+
- **loopx:plan-to-exec** - Creates the plan this skill executes
|
|
287
287
|
- **loopx:review** - Code review template for reviewer subagents
|
|
288
288
|
- **loopx:final-review** - Final whole-feature runtime and integration risk review
|
|
289
289
|
- **loopx:finish** - Complete development after all tasks
|
package/skills/tdd/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: tdd
|
|
|
3
3
|
description: "Guides feature and bugfix implementation through a failing test before production code and red-green-refactor discipline. Not for generated files or throwaway prototypes."
|
|
4
4
|
when_to_use: "tdd, failing test first, feature implementation, bugfix, regression test, red green refactor, 测试先行"
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.2.
|
|
6
|
+
version: "0.2.9"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Test-Driven Development (TDD)
|
package/skills/verify/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: verify
|
|
|
3
3
|
description: "Requires fresh verification evidence before claiming work is complete, fixed, passing, review-ready, or ready to commit. Not for speculative confidence or stale results."
|
|
4
4
|
when_to_use: "verify, completion claim, fixed claim, tests pass, review-ready, commit, fresh evidence, 验证, 完成前检查"
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.2.
|
|
6
|
+
version: "0.2.9"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Verification Before Completion
|
package/src/cli.mjs
CHANGED
|
@@ -30,7 +30,7 @@ function usage() {
|
|
|
30
30
|
' loopx status [slug] [--json]',
|
|
31
31
|
' loopx next <slug> [--json]',
|
|
32
32
|
' loopx setup-context',
|
|
33
|
-
' loopx install-skills [--target <codex|claude|all>] [--project] [--mode <copy|symlink>] [--dir <path>] [--yes] [--dry-run] [--json]',
|
|
33
|
+
' loopx install-skills [--target <codex|claude|all>] [--project] [--mode <copy|symlink>] [--dir <path>] [--add-agent-guidance] [--yes] [--dry-run] [--json]',
|
|
34
34
|
' loopx doctor [--json]',
|
|
35
35
|
' loopx migrate',
|
|
36
36
|
' loopx repair-install',
|
|
@@ -60,6 +60,7 @@ async function promptInstallOptions() {
|
|
|
60
60
|
const targetAnswer = (await rl.question('Install targets (codex, claude, all) [all]: ')).trim().toLowerCase();
|
|
61
61
|
const projectAnswer = (await rl.question('Install Claude project skills instead of user skills? [y/N]: ')).trim().toLowerCase();
|
|
62
62
|
const modeAnswer = (await rl.question('Install mode (copy, symlink) [copy]: ')).trim().toLowerCase();
|
|
63
|
+
const guidanceAnswer = (await rl.question('Add loopx guidance to Codex AGENTS.md / Claude CLAUDE.md? [y/N]: ')).trim().toLowerCase();
|
|
63
64
|
const proceedAnswer = (await rl.question('Proceed? [y/N]: ')).trim().toLowerCase();
|
|
64
65
|
if (proceedAnswer !== 'y' && proceedAnswer !== 'yes') {
|
|
65
66
|
return null;
|
|
@@ -69,6 +70,7 @@ async function promptInstallOptions() {
|
|
|
69
70
|
targets: target === 'all' ? ['codex', 'claude'] : [target],
|
|
70
71
|
project: projectAnswer === 'y' || projectAnswer === 'yes',
|
|
71
72
|
installMethod: modeAnswer === 'symlink' ? 'symlink' : 'copy',
|
|
73
|
+
agentGuidance: guidanceAnswer === 'y' || guidanceAnswer === 'yes',
|
|
72
74
|
};
|
|
73
75
|
} finally {
|
|
74
76
|
rl.close();
|
|
@@ -83,6 +85,7 @@ function installOptionsFromArgs(options) {
|
|
|
83
85
|
project: Boolean(options.get('--project')),
|
|
84
86
|
installMethod: options.get('--mode') === 'symlink' ? 'symlink' : 'copy',
|
|
85
87
|
dir: options.get('--dir'),
|
|
88
|
+
agentGuidance: Boolean(options.get('--add-agent-guidance') || options.get('--add-codex-agents-guidance')),
|
|
86
89
|
};
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -272,10 +275,10 @@ function humanMissingArtifactsText(status) {
|
|
|
272
275
|
function humanNextAction(status) {
|
|
273
276
|
const state = status.state || null;
|
|
274
277
|
if (state?.current_stage === 'clarify') {
|
|
275
|
-
if (nextSkillCommand(state)?.startsWith('$plan ')) {
|
|
276
|
-
return `Follow $plan ${state.slug}.`;
|
|
278
|
+
if (nextSkillCommand(state)?.startsWith('$plan-to-exec ')) {
|
|
279
|
+
return `Follow $plan-to-exec ${state.slug}.`;
|
|
277
280
|
}
|
|
278
|
-
return 'Finish clarification, then follow $plan when ready.';
|
|
281
|
+
return 'Finish clarification, then follow $plan-to-exec when ready.';
|
|
279
282
|
}
|
|
280
283
|
const payload = nextPayloadFromStatus(status, { human: true });
|
|
281
284
|
if (payload.next_skill_command) {
|
package/src/context-manifest.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
|
|
5
|
+
import { discoverLoopxContextArtifacts } from './loopx-context-artifacts.mjs';
|
|
5
6
|
import { inspectWorkspaceContext, resolveWorkspaceContextPaths } from './workspace-context.mjs';
|
|
6
7
|
|
|
7
8
|
export const CONTEXT_MANIFEST_SCHEMA_VERSION = 1;
|
|
@@ -64,6 +65,53 @@ function stableRows(rows) {
|
|
|
64
65
|
.slice(0, MAX_MANIFEST_ROWS);
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
async function loopxRepoContextRows(cwd, stage, priorityStart) {
|
|
69
|
+
const artifacts = await discoverLoopxContextArtifacts(cwd);
|
|
70
|
+
const rows = [];
|
|
71
|
+
let priority = priorityStart;
|
|
72
|
+
if (artifacts.specsRoot) {
|
|
73
|
+
rows.push(row(cwd, {
|
|
74
|
+
stage,
|
|
75
|
+
kind: 'repo-specs',
|
|
76
|
+
path: artifacts.specsRoot,
|
|
77
|
+
reason: 'long_lived_loopx_specs_directory',
|
|
78
|
+
priority: priority++,
|
|
79
|
+
required: false,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
for (const spec of artifacts.specFiles) {
|
|
83
|
+
rows.push(row(cwd, {
|
|
84
|
+
stage,
|
|
85
|
+
kind: 'repo-spec',
|
|
86
|
+
path: spec.path,
|
|
87
|
+
reason: 'long_lived_loopx_spec',
|
|
88
|
+
priority: priority++,
|
|
89
|
+
required: false,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
if (artifacts.memorySummary) {
|
|
93
|
+
rows.push(row(cwd, {
|
|
94
|
+
stage,
|
|
95
|
+
kind: 'memory-summary',
|
|
96
|
+
path: artifacts.memorySummary.path,
|
|
97
|
+
reason: 'curated_loopx_project_memory',
|
|
98
|
+
priority: priority++,
|
|
99
|
+
required: false,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
if (artifacts.memoryIndex) {
|
|
103
|
+
rows.push(row(cwd, {
|
|
104
|
+
stage,
|
|
105
|
+
kind: 'memory-index',
|
|
106
|
+
path: artifacts.memoryIndex.path,
|
|
107
|
+
reason: 'curated_loopx_memory_retrieval_index',
|
|
108
|
+
priority: priority++,
|
|
109
|
+
required: false,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
114
|
+
|
|
67
115
|
export async function writeContextManifest(path, rows) {
|
|
68
116
|
const text = stableRows(rows).map((item) => JSON.stringify(item)).join('\n');
|
|
69
117
|
await mkdir(dirname(path), { recursive: true });
|
|
@@ -141,6 +189,7 @@ export async function generateBuildContextManifest({ cwd, root, state, slug }) {
|
|
|
141
189
|
row(cwd, { stage: 'build', kind: 'domain-context', path: contextPaths.domainGlossary, reason: 'domain_vocabulary', priority: 34, required: contextSetup.status !== 'missing' }),
|
|
142
190
|
row(cwd, { stage: 'build', kind: 'agent-domain', path: contextPaths.agentDomain, reason: 'agent_context_rules', priority: 35, required: false }),
|
|
143
191
|
row(cwd, { stage: 'build', kind: 'workspace-config', path: join(cwd, '.loopx', 'config.json'), reason: 'project_rules_spec_sources_and_verification_commands', priority: 36, required: false }),
|
|
192
|
+
...await loopxRepoContextRows(cwd, 'build', 37),
|
|
144
193
|
];
|
|
145
194
|
const manifestPath = buildContextManifestPath(root);
|
|
146
195
|
await writeContextManifest(manifestPath, rows);
|
|
@@ -163,7 +212,8 @@ export async function generateReviewContextManifest({ cwd, root, state, slug })
|
|
|
163
212
|
row(cwd, { stage: 'review', kind: 'build-support', path: join(root, 'build-support'), reason: 'build_gate_evidence', priority: 30, required: false }),
|
|
164
213
|
row(cwd, { stage: 'review', kind: 'agent-domain', path: contextPaths.agentDomain, reason: 'agent_context_rules', priority: 31, required: false }),
|
|
165
214
|
row(cwd, { stage: 'review', kind: 'workspace-config', path: join(cwd, '.loopx', 'config.json'), reason: 'project_rules_spec_sources_and_verification_commands', priority: 32, required: false }),
|
|
166
|
-
|
|
215
|
+
...await loopxRepoContextRows(cwd, 'review', 33),
|
|
216
|
+
row(cwd, { stage: 'review', kind: 'state', path: join(root, 'state.json'), reason: 'workflow_state', priority: 60 }),
|
|
167
217
|
];
|
|
168
218
|
const manifestPath = reviewContextManifestPath(root);
|
|
169
219
|
await writeContextManifest(manifestPath, rows);
|
|
@@ -17,7 +17,7 @@ const PROJECT_ROOT = resolve(MODULE_DIR, '..');
|
|
|
17
17
|
const LOOPX_SKILLS = [
|
|
18
18
|
'clarify',
|
|
19
19
|
'spec',
|
|
20
|
-
'plan',
|
|
20
|
+
'plan-to-exec',
|
|
21
21
|
'subagent-exec',
|
|
22
22
|
'exec',
|
|
23
23
|
'review',
|
|
@@ -49,6 +49,18 @@ const LOOPX_MANAGED_SCRIPT_ITEMS = [
|
|
|
49
49
|
targetRelativePath: '.claude/hooks/loopx-workflow-hook.mjs',
|
|
50
50
|
},
|
|
51
51
|
];
|
|
52
|
+
const LOOPX_AGENT_GUIDANCE_BLOCK_ID = 'specs-and-memory-context';
|
|
53
|
+
const LOOPX_AGENT_GUIDANCE_HEADING = '## loopx Specs And Memory';
|
|
54
|
+
const LOOPX_AGENT_GUIDANCE_CONTENT = [
|
|
55
|
+
LOOPX_AGENT_GUIDANCE_HEADING,
|
|
56
|
+
'',
|
|
57
|
+
'When working in a repository that uses loopx:',
|
|
58
|
+
'',
|
|
59
|
+
'- If `docs/loopx/specs/` exists, inspect relevant specs before clarify, spec, plan, implementation, or review. Use `docs/loopx/specs/index.md` as a map when present, but do not require it.',
|
|
60
|
+
'- If `.loopx/memory/MEMORY.md` exists, read it as curated project memory.',
|
|
61
|
+
'- If `.loopx/memory/index.jsonl` exists, use it only to find relevant active memory cards.',
|
|
62
|
+
'- Treat current user instructions and named source documents as highest priority, repo specs as binding long-lived rules, and memory as advisory context.',
|
|
63
|
+
].join('\n');
|
|
52
64
|
const LOOPX_GOVERNED_SOURCE_ITEMS = [
|
|
53
65
|
{
|
|
54
66
|
name: 'loopx-plugin-manifest',
|
|
@@ -128,6 +140,19 @@ export function getClaudeSettingsPath(env = process.env) {
|
|
|
128
140
|
return resolve(env.LOOPX_CLAUDE_SETTINGS_PATH || join(home, '.claude', 'settings.json'));
|
|
129
141
|
}
|
|
130
142
|
|
|
143
|
+
export function getCodexAgentsPath(env = process.env) {
|
|
144
|
+
const home = resolve(env.LOOPX_HOME || env.HOME || process.cwd());
|
|
145
|
+
return resolve(env.LOOPX_CODEX_AGENTS_PATH || join(home, '.codex', 'AGENTS.md'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getClaudeAgentsPath(env = process.env, options = {}) {
|
|
149
|
+
if (options.project === true) {
|
|
150
|
+
return resolve(env.LOOPX_INSTALL_CWD || process.cwd(), 'CLAUDE.md');
|
|
151
|
+
}
|
|
152
|
+
const home = resolve(env.LOOPX_HOME || env.HOME || process.cwd());
|
|
153
|
+
return resolve(env.LOOPX_CLAUDE_AGENTS_PATH || join(home, '.claude', 'CLAUDE.md'));
|
|
154
|
+
}
|
|
155
|
+
|
|
131
156
|
export function getSkillLockPath(env = process.env) {
|
|
132
157
|
return resolve(env.LOOPX_SKILL_LOCK_PATH || join(getAgentsRoot(env), '.skill-lock.json'));
|
|
133
158
|
}
|
|
@@ -454,6 +479,85 @@ async function removeInstalledFile(path) {
|
|
|
454
479
|
await rm(path, { force: true });
|
|
455
480
|
}
|
|
456
481
|
|
|
482
|
+
function managedBlockMarkers(id) {
|
|
483
|
+
return {
|
|
484
|
+
start: `<!-- loopx:managed:block ${id} -->`,
|
|
485
|
+
end: `<!-- /loopx:managed:block ${id} -->`,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function renderManagedBlock(id, content) {
|
|
490
|
+
const markers = managedBlockMarkers(id);
|
|
491
|
+
return `${markers.start}\n${content.trim()}\n${markers.end}`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function managedBlockPattern(id) {
|
|
495
|
+
const escaped = String(id).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
496
|
+
return new RegExp(`<!--\\s*loopx:managed:block\\s+${escaped}\\s*-->[\\s\\S]*?<!--\\s*\\/loopx:managed:block\\s+${escaped}\\s*-->`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function upsertManagedBlock(existing, id, content) {
|
|
500
|
+
const nextBlock = renderManagedBlock(id, content);
|
|
501
|
+
const pattern = managedBlockPattern(id);
|
|
502
|
+
if (pattern.test(existing)) {
|
|
503
|
+
const nextContent = existing.replace(pattern, nextBlock);
|
|
504
|
+
return {
|
|
505
|
+
content: nextContent,
|
|
506
|
+
changed: nextContent !== existing,
|
|
507
|
+
existed: true,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
const trimmed = existing.trimEnd();
|
|
511
|
+
const contentWithBlock = trimmed
|
|
512
|
+
? `${trimmed}\n\n${nextBlock}\n`
|
|
513
|
+
: `${nextBlock}\n`;
|
|
514
|
+
return {
|
|
515
|
+
content: contentWithBlock,
|
|
516
|
+
changed: true,
|
|
517
|
+
existed: false,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export async function installAgentGuidanceFile(path, options = {}) {
|
|
522
|
+
const content = options.content || LOOPX_AGENT_GUIDANCE_CONTENT;
|
|
523
|
+
const id = options.id || LOOPX_AGENT_GUIDANCE_BLOCK_ID;
|
|
524
|
+
const existing = existsSync(path) ? await readFile(path, 'utf8') : '';
|
|
525
|
+
const existed = existsSync(path);
|
|
526
|
+
const next = upsertManagedBlock(existing, id, content);
|
|
527
|
+
if (!next.changed) {
|
|
528
|
+
return { status: 'already-current', path };
|
|
529
|
+
}
|
|
530
|
+
await ensureDir(dirname(path));
|
|
531
|
+
await writeFile(path, `${next.content.replace(/\s+$/, '')}\n`);
|
|
532
|
+
return {
|
|
533
|
+
status: next.existed ? 'updated' : (existed ? 'installed' : 'created'),
|
|
534
|
+
path,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function agentGuidanceEnabled(options = {}) {
|
|
539
|
+
return Boolean(options.agentGuidance || options.codexAgentsGuidance);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export async function installAgentGuidance(env = process.env, options = {}) {
|
|
543
|
+
const target = options.target || env.LOOPX_INSTALL_TARGET || 'codex';
|
|
544
|
+
const enabled = agentGuidanceEnabled(options);
|
|
545
|
+
const result = {};
|
|
546
|
+
if (target === 'codex' || target === 'all') {
|
|
547
|
+
const path = getCodexAgentsPath(env);
|
|
548
|
+
result.codex = enabled
|
|
549
|
+
? await installAgentGuidanceFile(path)
|
|
550
|
+
: { status: 'recommended', path };
|
|
551
|
+
}
|
|
552
|
+
if (target === 'claude' || target === 'all') {
|
|
553
|
+
const path = getClaudeAgentsPath(env, options);
|
|
554
|
+
result.claude = enabled
|
|
555
|
+
? await installAgentGuidanceFile(path)
|
|
556
|
+
: { status: 'recommended', path };
|
|
557
|
+
}
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
|
|
457
561
|
async function canonicalTargetOwnership(skillName, env = process.env, options = {}) {
|
|
458
562
|
const targetDir = installedSkillDir(skillName, env);
|
|
459
563
|
const sourceDir = skillSourceDir(skillName, env, options.skillSourceRoot);
|
|
@@ -736,11 +840,16 @@ export async function installBundledSkills(env = process.env, options = {}) {
|
|
|
736
840
|
items: nextTemplateItems,
|
|
737
841
|
});
|
|
738
842
|
const templateGovernance = await inspectTemplateGovernance(baselinePath);
|
|
843
|
+
const agentGuidance = await installAgentGuidance(env, {
|
|
844
|
+
...options,
|
|
845
|
+
target: options.target || env.LOOPX_INSTALL_TARGET || 'codex',
|
|
846
|
+
});
|
|
739
847
|
return {
|
|
740
848
|
ok: conflicts.length === 0,
|
|
741
849
|
installed,
|
|
742
850
|
conflicts,
|
|
743
851
|
skipped,
|
|
852
|
+
agentGuidance,
|
|
744
853
|
templateGovernance,
|
|
745
854
|
inspection: await inspectInstallState(env),
|
|
746
855
|
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const MAX_SPEC_CONTEXT_FILES = 12;
|
|
6
|
+
|
|
7
|
+
function displayPath(cwd, path) {
|
|
8
|
+
const rel = relative(cwd, path);
|
|
9
|
+
return rel && !rel.startsWith('..') ? rel : path;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeChangedFiles(files = []) {
|
|
13
|
+
return Array.isArray(files)
|
|
14
|
+
? files.map((file) => String(file || '').trim()).filter(Boolean)
|
|
15
|
+
: [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function listMarkdownFiles(root) {
|
|
19
|
+
if (!existsSync(root)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const found = [];
|
|
23
|
+
async function walk(dir) {
|
|
24
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
25
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
26
|
+
const path = join(dir, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
await walk(path);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (entry.isFile() && /\.md$/i.test(entry.name)) {
|
|
32
|
+
found.push(path);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
await walk(root);
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pathParts(value) {
|
|
41
|
+
return String(value || '')
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.split(/[^a-z0-9]+/)
|
|
44
|
+
.filter((part) => part.length >= 3);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function frontmatterAppliesTo(text) {
|
|
48
|
+
if (!String(text || '').startsWith('---\n')) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const end = text.indexOf('\n---\n', 4);
|
|
52
|
+
if (end === -1) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const lines = text.slice(4, end).split('\n');
|
|
56
|
+
const values = [];
|
|
57
|
+
let inAppliesTo = false;
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
if (/^applies_to:\s*$/.test(line)) {
|
|
60
|
+
inAppliesTo = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (inAppliesTo && /^\s+-\s+/.test(line)) {
|
|
64
|
+
values.push(line.replace(/^\s+-\s+/, '').trim().replace(/^['"]|['"]$/g, ''));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (inAppliesTo && /^\S/.test(line)) {
|
|
68
|
+
inAppliesTo = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return values.filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function appliesToChangedFile(pattern, changedFile) {
|
|
75
|
+
const normalizedPattern = String(pattern || '').replace(/\*\*?\/?/g, '').replace(/\/+$/, '');
|
|
76
|
+
const normalizedFile = String(changedFile || '');
|
|
77
|
+
return normalizedPattern && normalizedFile.includes(normalizedPattern);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function specRecord(cwd, path, changedFiles) {
|
|
81
|
+
const text = await readFile(path, 'utf8');
|
|
82
|
+
const appliesTo = frontmatterAppliesTo(text);
|
|
83
|
+
const stemParts = pathParts(basename(path, '.md'));
|
|
84
|
+
const changedParts = new Set(changedFiles.flatMap(pathParts));
|
|
85
|
+
const filenameMatch = stemParts.some((part) => changedParts.has(part));
|
|
86
|
+
const appliesToMatch = appliesTo.some((pattern) => changedFiles.some((file) => appliesToChangedFile(pattern, file)));
|
|
87
|
+
const isIndex = /(^|\/)index\.md$/i.test(path);
|
|
88
|
+
const isInbox = /(^|\/)inbox\.md$/i.test(path);
|
|
89
|
+
return {
|
|
90
|
+
path: displayPath(cwd, path),
|
|
91
|
+
appliesTo,
|
|
92
|
+
relevant: isIndex || isInbox || filenameMatch || appliesToMatch || changedFiles.length === 0,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function discoverLoopxContextArtifacts(cwd, options = {}) {
|
|
97
|
+
const root = resolve(cwd);
|
|
98
|
+
const changedFiles = normalizeChangedFiles(options.changedFiles);
|
|
99
|
+
const specsRootPath = join(root, 'docs', 'loopx', 'specs');
|
|
100
|
+
const specPaths = await listMarkdownFiles(specsRootPath);
|
|
101
|
+
const records = await Promise.all(specPaths.map((path) => specRecord(root, path, changedFiles)));
|
|
102
|
+
const relevantSpecs = records
|
|
103
|
+
.filter((record) => record.relevant)
|
|
104
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
105
|
+
.slice(0, MAX_SPEC_CONTEXT_FILES);
|
|
106
|
+
const memorySummaryPath = join(root, '.loopx', 'memory', 'MEMORY.md');
|
|
107
|
+
const memoryIndexPath = join(root, '.loopx', 'memory', 'index.jsonl');
|
|
108
|
+
return {
|
|
109
|
+
specsRoot: existsSync(specsRootPath) ? displayPath(root, specsRootPath) : null,
|
|
110
|
+
specFiles: relevantSpecs,
|
|
111
|
+
memorySummary: existsSync(memorySummaryPath) ? { path: displayPath(root, memorySummaryPath) } : null,
|
|
112
|
+
memoryIndex: existsSync(memoryIndexPath) ? { path: displayPath(root, memoryIndexPath) } : null,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/src/next-skill.mjs
CHANGED
|
@@ -9,7 +9,7 @@ export function nextSkillCommand(state) {
|
|
|
9
9
|
&& state.clarify_decision_boundaries_resolved === true
|
|
10
10
|
&& state.clarify_pressure_pass_complete === true
|
|
11
11
|
) {
|
|
12
|
-
return `$plan ${state.slug}`;
|
|
12
|
+
return `$plan-to-exec ${state.slug}`;
|
|
13
13
|
}
|
|
14
14
|
if (state.current_stage === 'done'
|
|
15
15
|
&& state.completion_confirmed === true) {
|
|
@@ -51,7 +51,7 @@ export function nextSkillCommand(state) {
|
|
|
51
51
|
&& state.review_verdict === 'request-changes'
|
|
52
52
|
&& state.requested_transition === 'review->plan'
|
|
53
53
|
&& state.approval?.rollback === 'approved') {
|
|
54
|
-
return `$plan ${state.slug}`;
|
|
54
|
+
return `$plan-to-exec ${state.slug}`;
|
|
55
55
|
}
|
|
56
56
|
if (state.current_stage === 'review'
|
|
57
57
|
&& state.review_verdict === 'request-changes'
|
|
@@ -67,6 +67,7 @@ async function discoverSpecSources(cwd) {
|
|
|
67
67
|
candidate(join(cwd, 'specs'), 'specs'),
|
|
68
68
|
candidate(join(cwd, 'docs', 'changes'), 'docs/changes'),
|
|
69
69
|
candidate(join(cwd, 'docs', 'specs'), 'docs/specs'),
|
|
70
|
+
candidate(join(cwd, 'docs', 'loopx', 'specs'), 'docs/loopx/specs'),
|
|
70
71
|
candidate(join(cwd, 'docs', 'adr'), 'docs/adr'),
|
|
71
72
|
candidate(join(cwd, 'docs', 'rfcs'), 'docs/rfcs'),
|
|
72
73
|
]);
|
package/src/workflow.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { inspectProjectConventions } from './project-discovery.mjs';
|
|
|
20
20
|
import { createDefaultReviewAdapter } from './review-runtime.mjs';
|
|
21
21
|
import { appendWorkspaceJournal } from './workspace-memory.mjs';
|
|
22
22
|
import { inspectWorkspaceContext, setupWorkspaceContext } from './workspace-context.mjs';
|
|
23
|
+
import { discoverLoopxContextArtifacts } from './loopx-context-artifacts.mjs';
|
|
23
24
|
|
|
24
25
|
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const WORKSPACE_SCHEMA_VERSION = 1;
|
|
@@ -325,9 +326,6 @@ function compactPlanningText(text, { html = false } = {}) {
|
|
|
325
326
|
async function readPlanSourceText(cwd, state, sourceSpecPath) {
|
|
326
327
|
const sourceText = await readFile(sourceSpecPath, 'utf8');
|
|
327
328
|
const sourceDocumentPaths = sourceDocumentPathsFromSpecAndState(sourceSpecPath, sourceText, state);
|
|
328
|
-
if (sourceDocumentPaths.length === 0) {
|
|
329
|
-
return { sourceText, sourceDocumentPaths: [] };
|
|
330
|
-
}
|
|
331
329
|
|
|
332
330
|
const parts = [sourceText.trimEnd()];
|
|
333
331
|
const loaded = [];
|
|
@@ -350,6 +348,11 @@ async function readPlanSourceText(cwd, state, sourceSpecPath) {
|
|
|
350
348
|
break;
|
|
351
349
|
}
|
|
352
350
|
}
|
|
351
|
+
const repoContext = await readLoopxRepoContextText(cwd, sourceSpecPath);
|
|
352
|
+
if (repoContext.text) {
|
|
353
|
+
parts.push(repoContext.text);
|
|
354
|
+
loaded.push(...repoContext.paths);
|
|
355
|
+
}
|
|
353
356
|
|
|
354
357
|
return {
|
|
355
358
|
sourceText: parts.join('\n\n').slice(0, MAX_PLAN_SOURCE_BUNDLE_CHARS),
|
|
@@ -357,6 +360,47 @@ async function readPlanSourceText(cwd, state, sourceSpecPath) {
|
|
|
357
360
|
};
|
|
358
361
|
}
|
|
359
362
|
|
|
363
|
+
async function readLoopxRepoContextText(cwd, sourceSpecPath) {
|
|
364
|
+
const artifacts = await discoverLoopxContextArtifacts(cwd, {
|
|
365
|
+
changedFiles: [relative(cwd, sourceSpecPath)],
|
|
366
|
+
});
|
|
367
|
+
const paths = [
|
|
368
|
+
...artifacts.specFiles.map((item) => item.path),
|
|
369
|
+
artifacts.memorySummary?.path,
|
|
370
|
+
].filter(Boolean);
|
|
371
|
+
if (paths.length === 0) {
|
|
372
|
+
return { text: '', paths: [] };
|
|
373
|
+
}
|
|
374
|
+
const sections = [];
|
|
375
|
+
const loaded = [];
|
|
376
|
+
for (const display of paths) {
|
|
377
|
+
const absolute = resolve(cwd, display);
|
|
378
|
+
if (!existsSync(absolute)) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const raw = await readFile(absolute, 'utf8');
|
|
382
|
+
loaded.push(absolute);
|
|
383
|
+
sections.push([
|
|
384
|
+
`# loopx context: ${display}`,
|
|
385
|
+
'',
|
|
386
|
+
compactPlanningText(raw),
|
|
387
|
+
].join('\n'));
|
|
388
|
+
}
|
|
389
|
+
if (sections.length === 0) {
|
|
390
|
+
return { text: '', paths: [] };
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
text: [
|
|
394
|
+
'# loopx Repo Specs And Memory Context',
|
|
395
|
+
'',
|
|
396
|
+
'Current task instructions and named source documents have priority. Repo specs are binding long-lived rules. Memory is advisory.',
|
|
397
|
+
'',
|
|
398
|
+
...sections,
|
|
399
|
+
].join('\n\n'),
|
|
400
|
+
paths: loaded,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
360
404
|
function frontmatterBlock(values) {
|
|
361
405
|
const lines = ['---'];
|
|
362
406
|
for (const [key, value] of Object.entries(values)) {
|
|
@@ -3131,8 +3175,8 @@ function recommendedAction(state, legacy = false) {
|
|
|
3131
3175
|
switch (state.current_stage) {
|
|
3132
3176
|
case STAGES.CLARIFY:
|
|
3133
3177
|
return state.approval.plan === APPROVAL_STATES.APPROVED
|
|
3134
|
-
? `Follow $plan ${state.slug}.`
|
|
3135
|
-
: 'Finish clarification, then follow $plan when ready.';
|
|
3178
|
+
? `Follow $plan-to-exec ${state.slug}.`
|
|
3179
|
+
: 'Finish clarification, then follow $plan-to-exec when ready.';
|
|
3136
3180
|
case STAGES.PLAN:
|
|
3137
3181
|
if (Array.isArray(state.plan_blockers) && state.plan_blockers.length > 0) {
|
|
3138
3182
|
return 'Run loopx plan to continue the planning review loop until architect, critic, and planning artifact blockers are cleared.';
|
|
@@ -3356,7 +3400,7 @@ function nextCommandForRollbackTarget(slug, target) {
|
|
|
3356
3400
|
return [
|
|
3357
3401
|
'Next:',
|
|
3358
3402
|
`loopx approve ${slug} --from review --to plan`,
|
|
3359
|
-
`$plan ${slug}`,
|
|
3403
|
+
`$plan-to-exec ${slug}`,
|
|
3360
3404
|
].join('\n');
|
|
3361
3405
|
}
|
|
3362
3406
|
|
|
@@ -3936,7 +3980,7 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
3936
3980
|
&& state.approval.plan === APPROVAL_STATES.APPROVED;
|
|
3937
3981
|
if (!options.directSpecPath) {
|
|
3938
3982
|
if (consumesReviewPlan || resumesConsumedReviewPlan || resumesClarifyPlan) {
|
|
3939
|
-
// A no-go review or a blocked planning run may route back to plan; the printed Next command is $plan.
|
|
3983
|
+
// A no-go review or a blocked planning run may route back to plan; the printed Next command is $plan-to-exec.
|
|
3940
3984
|
} else {
|
|
3941
3985
|
ensureApprovedTransition(state, TRANSITIONS.CLARIFY_TO_PLAN, 'plan');
|
|
3942
3986
|
}
|