@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.
Files changed (52) hide show
  1. package/README.md +21 -8
  2. package/README.zh-CN.md +22 -9
  3. 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
  4. package/docs/loopx/design/loopx-skill-suite-v1-design.md +4 -4
  5. package/docs/loopx/plans/2026-06-14-loopx-spec-memory-context-loading.md +948 -0
  6. package/docs/loopx/plans/loopx-skill-suite-v1-implementation.md +1 -1
  7. package/package.json +2 -2
  8. package/plugins/loopx/.codex-plugin/plugin.json +1 -1
  9. package/plugins/loopx/skills/clarify/SKILL.md +14 -3
  10. package/plugins/loopx/skills/debug/SKILL.md +1 -1
  11. package/plugins/loopx/skills/doc-readability/SKILL.md +1 -1
  12. package/plugins/loopx/skills/exec/SKILL.md +2 -2
  13. package/plugins/loopx/skills/final-review/SKILL.md +1 -1
  14. package/plugins/loopx/skills/finish/SKILL.md +1 -1
  15. package/plugins/loopx/skills/fix-review/SKILL.md +1 -1
  16. package/plugins/loopx/skills/go-style/SKILL.md +1 -1
  17. package/plugins/loopx/skills/kratos/SKILL.md +1 -1
  18. package/plugins/loopx/skills/{plan → plan-to-exec}/SKILL.md +16 -5
  19. package/plugins/loopx/skills/refactor-plan/SKILL.md +1 -1
  20. package/plugins/loopx/skills/review/SKILL.md +1 -1
  21. package/plugins/loopx/skills/spec/DESIGN_SPEC_TEMPLATE.md +2 -2
  22. package/plugins/loopx/skills/spec/SKILL.md +15 -4
  23. package/plugins/loopx/skills/subagent-exec/SKILL.md +2 -2
  24. package/plugins/loopx/skills/tdd/SKILL.md +1 -1
  25. package/plugins/loopx/skills/verify/SKILL.md +1 -1
  26. package/scripts/claude-workflow-hook.mjs +2 -2
  27. package/scripts/codex-workflow-hook.mjs +4 -4
  28. package/skills/RESOLVER.md +4 -4
  29. package/skills/clarify/SKILL.md +14 -3
  30. package/skills/debug/SKILL.md +1 -1
  31. package/skills/doc-readability/SKILL.md +1 -1
  32. package/skills/exec/SKILL.md +2 -2
  33. package/skills/final-review/SKILL.md +1 -1
  34. package/skills/finish/SKILL.md +1 -1
  35. package/skills/fix-review/SKILL.md +1 -1
  36. package/skills/go-style/SKILL.md +1 -1
  37. package/skills/kratos/SKILL.md +1 -1
  38. package/skills/{plan → plan-to-exec}/SKILL.md +16 -5
  39. package/skills/refactor-plan/SKILL.md +1 -1
  40. package/skills/review/SKILL.md +1 -1
  41. package/skills/spec/DESIGN_SPEC_TEMPLATE.md +2 -2
  42. package/skills/spec/SKILL.md +15 -4
  43. package/skills/subagent-exec/SKILL.md +2 -2
  44. package/skills/tdd/SKILL.md +1 -1
  45. package/skills/verify/SKILL.md +1 -1
  46. package/src/cli.mjs +7 -4
  47. package/src/context-manifest.mjs +51 -1
  48. package/src/install-discovery.mjs +110 -1
  49. package/src/loopx-context-artifacts.mjs +114 -0
  50. package/src/next-skill.mjs +2 -2
  51. package/src/project-discovery.mjs +1 -0
  52. package/src/workflow.mjs +51 -7
@@ -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.7"
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.7"
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
@@ -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.7"
6
+ version: "0.2.9"
7
7
  ---
8
8
 
9
9
  # Test-Driven Development (TDD)
@@ -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.7"
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) {
@@ -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
- row(cwd, { stage: 'review', kind: 'state', path: join(root, 'state.json'), reason: 'workflow_state', priority: 40 }),
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
+ }
@@ -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
  }