dev-loops 0.2.0 → 0.2.2
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/.claude/.claude-plugin/plugin.json +5 -1
- package/.claude/skills/dev-loop/SKILL.md +2 -2
- package/.claude-plugin/marketplace.json +19 -0
- package/CHANGELOG.md +44 -0
- package/README.md +17 -5
- package/cli/index.mjs +2 -2
- package/package.json +6 -12
- package/scripts/github/reply-resolve-review-thread.mjs +89 -30
- package/scripts/loop/checkpoint-contract.mjs +79 -30
- package/scripts/loop/outer-loop.mjs +4 -1
- package/scripts/loop/resolve-dev-loop-startup.mjs +5 -1
- package/scripts/repo-wiki.mjs +183 -36
- package/skills/dev-loop/SKILL.md +2 -2
- package/scripts/repo-wiki-local.mjs +0 -156
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev-loops",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Agent-harness-agnostic dev-loop: agents, skills, and hooks for GitHub/Copilot-driven development loops.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Manuel Fittko",
|
|
7
|
+
"email": "manuel.fittko@sofatutor.com"
|
|
8
|
+
},
|
|
5
9
|
"license": "MIT",
|
|
6
10
|
"homepage": "https://github.com/mfittko/dev-loops#readme",
|
|
7
11
|
"repository": "https://github.com/mfittko/dev-loops.git",
|
|
@@ -25,7 +25,7 @@ Required installed runtime contract docs are shared bundled copies under `../doc
|
|
|
25
25
|
|
|
26
26
|
The main agent must **always** dispatch the `dev-loop` async subagent for any dev-loop work.
|
|
27
27
|
Do not run `dev-loops loop startup` or any startup resolver in the main agent.
|
|
28
|
-
|
|
28
|
+
For async-required routes (config `workflow.asyncStartMode`, default `required`) the resolver needs an async run-id marker (`DEVLOOPS_RUN_ID`, or the `PI_SUBAGENT_RUN_ID` alias) that the Pi harness injects when it dispatches the async subagent; under the Claude Code harness the requirement is relaxed automatically (no marker needed). The startup resolver also runs without a marker for non-async routes. Regardless, only the `dev-loop` subagent runs it — never the main agent.
|
|
29
29
|
|
|
30
30
|
### Dev-loop subagent (post-dispatch)
|
|
31
31
|
|
|
@@ -85,7 +85,7 @@ Do not preload route packs before the resolver selects the strategy.
|
|
|
85
85
|
|
|
86
86
|
## Async dispatch
|
|
87
87
|
|
|
88
|
-
**Async dispatch rule (enforced):** the resolver
|
|
88
|
+
**Async dispatch rule (enforced):** the resolver fails closed for GitHub-first strategies when `canonicalStateSummary.requiresAsyncDispatch` is `true` (default `required` mode) — inline invocation without an async run-id marker (`DEVLOOPS_RUN_ID`, or the `PI_SUBAGENT_RUN_ID` alias) is rejected for those routes. Under the Claude Code harness this requirement is relaxed automatically. See [Startup procedure](#startup-procedure).
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
## Fallback gate-comment poster
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dev-loops",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Manuel Fittko",
|
|
5
|
+
"email": "manuel.fittko@sofatutor.com"
|
|
6
|
+
},
|
|
7
|
+
"description": "Turn GitHub issues into merged PRs: agent-harness-agnostic dev-loop agents, skills, and hooks for Claude Code.",
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "dev-loops",
|
|
11
|
+
"source": "./.claude",
|
|
12
|
+
"description": "Agent-harness-agnostic dev-loop: agents, skills, and hooks for GitHub/Copilot-driven development loops.",
|
|
13
|
+
"homepage": "https://github.com/mfittko/dev-loops#readme",
|
|
14
|
+
"repository": "https://github.com/mfittko/dev-loops.git",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"keywords": ["dev-loop", "workflow", "github", "copilot", "claude-code", "plugin"]
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.2.2
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Claude Code: dev-loop no longer dead-ends on the async-start contract** (#830). Running
|
|
10
|
+
`/dev-loop` from the installed plugin failed immediately because `dev-loops loop startup`
|
|
11
|
+
enforces an async-start contract — it requires a run-id env marker (`DEVLOOPS_RUN_ID` /
|
|
12
|
+
`PI_SUBAGENT_RUN_ID`) that Pi injects when dispatching an async subagent but Claude Code's
|
|
13
|
+
Agent tool does not. That contract guards against detached, uninspectable background
|
|
14
|
+
processes, a risk that does not exist under Claude's Agent model (each subagent run is
|
|
15
|
+
visible and inspectable). The async requirement remains configurable via
|
|
16
|
+
`workflow.asyncStartMode` (`required` | `allowed`); under the Claude harness it is now
|
|
17
|
+
**relaxed to `allowed` at runtime** via `resolveEffectiveAsyncStartMode`, which consults the
|
|
18
|
+
new `isClaudeHarness` helper (`CLAUDECODE=1`) in `@dev-loops/core/loop/run-context`. An
|
|
19
|
+
explicit `DEVLOOPS_RUN_ID` still resolves as `valid`, and Pi behavior is unchanged (outside
|
|
20
|
+
Claude the configured mode is honored verbatim).
|
|
21
|
+
- The async-start CLI contract test is now hermetic — it clears `CLAUDECODE` (and the run-id
|
|
22
|
+
markers) so the rejection path is exercised regardless of the harness the suite runs under.
|
|
23
|
+
- The generated `dev-loop` skill prose no longer claims `PI_SUBAGENT_RUN_ID` is *required* — it
|
|
24
|
+
now describes the async run-id marker (`DEVLOOPS_RUN_ID` / `PI_SUBAGENT_RUN_ID` alias) and notes
|
|
25
|
+
the Claude-harness relaxation, so the plugin's docs match the runtime behavior. Subagent
|
|
26
|
+
spawning via the `dev-loop` agent is confirmed correctly wired: it grants the `Agent` tool
|
|
27
|
+
(the current subagent-spawning tool, renamed from `Task` in Claude Code v2.1.63) and the
|
|
28
|
+
strategy skills delegate to the worker agents (`developer`/`quality`/`refiner`/`fixer`/`review`/`docs`).
|
|
29
|
+
|
|
30
|
+
## 0.2.1
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **Claude Code marketplace catalog** (#828): ship `.claude-plugin/marketplace.json` at the repo
|
|
35
|
+
root so the repo can be added as a plugin marketplace (`/plugin marketplace add mfittko/dev-loops`,
|
|
36
|
+
or the *Manage Plugins → Marketplaces → Add* UI) and the plugin installed with
|
|
37
|
+
`/plugin install dev-loops@dev-loops`. The catalog's single plugin entry sources the existing
|
|
38
|
+
in-repo plugin at `./.claude`; the plugin version stays authoritative in `plugin.json`. A
|
|
39
|
+
contract test locks the catalog shape, and `.claude-plugin/` is added to the npm `files`
|
|
40
|
+
allowlist. Verified end-to-end with `claude plugin validate` + `marketplace add`/`install`
|
|
41
|
+
(4 skills, 7 agents, 2 hooks).
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- `plugin.json` now declares an `author` (clears the marketplace-validation warning).
|
|
46
|
+
- README "Claude Code plugin" section drops the `(preview)` framing and documents marketplace
|
|
47
|
+
install; the two CLI help lines that said plugin packaging was "in progress" are updated.
|
|
48
|
+
|
|
5
49
|
## 0.2.0
|
|
6
50
|
|
|
7
51
|
### Added — Claude Code harness (agent-harness-agnostic dev-loop)
|
package/README.md
CHANGED
|
@@ -45,11 +45,20 @@ Or run directly without installing:
|
|
|
45
45
|
npx dev-loops --help
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
### Claude Code plugin
|
|
48
|
+
### Claude Code plugin
|
|
49
49
|
|
|
50
50
|
The repo ships a Claude Code plugin rooted at `.claude/` (manifest at
|
|
51
|
-
`.claude/.claude-plugin/plugin.json`) exposing the dev-loop agents
|
|
52
|
-
|
|
51
|
+
`.claude/.claude-plugin/plugin.json`) exposing the dev-loop **agents, skills, and hooks**.
|
|
52
|
+
|
|
53
|
+
Install it from the bundled marketplace catalog (`.claude-plugin/marketplace.json`) by running
|
|
54
|
+
these slash commands inside Claude Code:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
/plugin marketplace add mfittko/dev-loops # register the marketplace
|
|
58
|
+
/plugin install dev-loops@dev-loops # install the plugin
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or load it directly for a single session without installing:
|
|
53
62
|
|
|
54
63
|
```bash
|
|
55
64
|
claude --plugin-dir .claude # load it for a session
|
|
@@ -58,8 +67,11 @@ claude --plugin-dir .claude plugin details dev-loops # inspect the discovere
|
|
|
58
67
|
|
|
59
68
|
When installed from npm, point at the bundled copy: `claude --plugin-dir node_modules/dev-loops/.claude`.
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
The plugin is self-contained: it bundles the shared contract docs and templates the skills
|
|
71
|
+
reference, and strips Pi-runtime-only prose from the generated assets. The hooks provide the
|
|
72
|
+
`gh pr ready` draft-gate guard and the main-agent read-only boundary (the read-only enforcement
|
|
73
|
+
is opt-in via `DEVLOOPS_MAIN_AGENT_READONLY=1`). Skill references to a project's own `PLAN.md` /
|
|
74
|
+
`AGENTS.md` resolve against the consumer repo, by design.
|
|
63
75
|
|
|
64
76
|
### Pi extension
|
|
65
77
|
|
package/cli/index.mjs
CHANGED
|
@@ -210,7 +210,7 @@ function buildCliHelpLines() {
|
|
|
210
210
|
"",
|
|
211
211
|
"`/dev-loops hide` remains an extension-only Pi command.",
|
|
212
212
|
"Run via `npx dev-loops` (or `npm install -g dev-loops` for the shell command); see the",
|
|
213
|
-
"README for Pi-extension
|
|
213
|
+
"README for Pi-extension and Claude Code plugin setup.",
|
|
214
214
|
];
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -232,7 +232,7 @@ function orderedCliSetupSteps(checks) {
|
|
|
232
232
|
return [
|
|
233
233
|
"1. Use `/dev-loop` (Claude Code) or `/skill:dev-loop` (Pi) to start or continue a dev loop — the single public entry.",
|
|
234
234
|
"2. Run `dev-loops status` whenever you want a concise readiness snapshot.",
|
|
235
|
-
"3. Run via `npx dev-loops` (or `npm install -g dev-loops` for the shell command); see the README for Pi-extension
|
|
235
|
+
"3. Run via `npx dev-loops` (or `npm install -g dev-loops` for the shell command); see the README for Pi-extension and Claude Code plugin setup.",
|
|
236
236
|
];
|
|
237
237
|
}
|
|
238
238
|
|
package/package.json
CHANGED
|
@@ -40,14 +40,9 @@
|
|
|
40
40
|
"repo-wiki:lint": "node scripts/repo-wiki.mjs lint --repo .",
|
|
41
41
|
"repo-wiki:search": "node scripts/repo-wiki.mjs search --repo .",
|
|
42
42
|
"repo-wiki:init": "node scripts/repo-wiki.mjs init --repo .",
|
|
43
|
-
"repo-wiki:
|
|
44
|
-
"repo-wiki:local
|
|
45
|
-
"repo-wiki:local-
|
|
46
|
-
"repo-wiki:local-plan": "node scripts/repo-wiki-local.mjs plan --repo .",
|
|
47
|
-
"repo-wiki:local-lint-docs": "node scripts/repo-wiki-local.mjs lint-docs --repo .",
|
|
48
|
-
"repo-wiki:local-compile": "node scripts/repo-wiki-local.mjs compile --repo .",
|
|
49
|
-
"repo-wiki:local-lint": "node scripts/repo-wiki-local.mjs lint --repo .",
|
|
50
|
-
"repo-wiki:prepare": "node scripts/repo-wiki-local.mjs prepare"
|
|
43
|
+
"repo-wiki:prepare": "node scripts/repo-wiki.mjs --source local prepare",
|
|
44
|
+
"repo-wiki:local": "node scripts/repo-wiki.mjs --source local",
|
|
45
|
+
"repo-wiki:local-prepare": "node scripts/repo-wiki.mjs --source local prepare"
|
|
51
46
|
},
|
|
52
47
|
"bin": {
|
|
53
48
|
"dev-loops": "./cli/index.mjs"
|
|
@@ -58,7 +53,6 @@
|
|
|
58
53
|
},
|
|
59
54
|
"devDependencies": {
|
|
60
55
|
"@playwright/test": "^1.60.0",
|
|
61
|
-
"c8": "^11.0.0",
|
|
62
56
|
"tsx": "^4.22.0"
|
|
63
57
|
},
|
|
64
58
|
"pi": {
|
|
@@ -74,8 +68,7 @@
|
|
|
74
68
|
},
|
|
75
69
|
"dependencies": {
|
|
76
70
|
"mermaid": "11.15.0",
|
|
77
|
-
"
|
|
78
|
-
"@dev-loops/core": "^0.2.0"
|
|
71
|
+
"@dev-loops/core": "^0.2.2"
|
|
79
72
|
},
|
|
80
73
|
"repository": {
|
|
81
74
|
"type": "git",
|
|
@@ -85,7 +78,7 @@
|
|
|
85
78
|
"url": "https://github.com/mfittko/dev-loops/issues"
|
|
86
79
|
},
|
|
87
80
|
"homepage": "https://github.com/mfittko/dev-loops#readme",
|
|
88
|
-
"version": "0.2.
|
|
81
|
+
"version": "0.2.2",
|
|
89
82
|
"files": [
|
|
90
83
|
"cli/",
|
|
91
84
|
"lib/",
|
|
@@ -93,6 +86,7 @@
|
|
|
93
86
|
"extension/",
|
|
94
87
|
"skills/",
|
|
95
88
|
"agents/",
|
|
89
|
+
".claude-plugin/",
|
|
96
90
|
".claude/.claude-plugin/",
|
|
97
91
|
".claude/agents/",
|
|
98
92
|
".claude/skills/",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { isDirectCliRun } from "@dev-loops/core/cli/helpers";
|
|
6
|
+
import { parsePositiveInteger } from "@dev-loops/core/cli/primitives";
|
|
4
7
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
8
|
import {
|
|
6
9
|
replyAndMaybeResolve,
|
|
@@ -9,34 +12,90 @@ import {
|
|
|
9
12
|
|
|
10
13
|
export { hasCommitShaReference } from "./_review-thread-mutations.mjs";
|
|
11
14
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
15
|
+
const USAGE = `Usage: dev-loops reply-resolve-review-thread --repo <owner/name> --pr <n> --comment-id <n> --thread-id <id> --body-file <path>
|
|
16
|
+
|
|
17
|
+
Reply to a review thread comment and resolve the thread.
|
|
18
|
+
|
|
19
|
+
Required:
|
|
20
|
+
--repo <owner/name> GitHub repository slug
|
|
21
|
+
--pr <n> Pull request number
|
|
22
|
+
--comment-id <n> GraphQL databaseId of the comment to reply to
|
|
23
|
+
--thread-id <id> GraphQL node ID of the review thread
|
|
24
|
+
--body-file <path> Path to file containing the reply body text`;
|
|
25
|
+
|
|
26
|
+
function parseError(message) {
|
|
27
|
+
return Object.assign(new Error(message), { usage: USAGE });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseCliArgs(argv) {
|
|
31
|
+
let values;
|
|
32
|
+
try {
|
|
33
|
+
({ values } = parseArgs({
|
|
34
|
+
args: argv,
|
|
35
|
+
options: {
|
|
36
|
+
repo: { type: "string" },
|
|
37
|
+
pr: { type: "string" },
|
|
38
|
+
"comment-id": { type: "string" },
|
|
39
|
+
"thread-id": { type: "string" },
|
|
40
|
+
"body-file": { type: "string" },
|
|
41
|
+
help: { type: "boolean", short: "h" },
|
|
42
|
+
},
|
|
43
|
+
strict: true,
|
|
44
|
+
allowPositionals: false,
|
|
45
|
+
}));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw parseError(err instanceof Error ? err.message : String(err));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (values.help) {
|
|
51
|
+
return { help: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!values.repo) throw parseError("Missing required option: --repo");
|
|
55
|
+
if (!values.pr) throw parseError("Missing required option: --pr");
|
|
56
|
+
if (!values["comment-id"]) throw parseError("Missing required option: --comment-id");
|
|
57
|
+
if (!values["thread-id"]) throw parseError("Missing required option: --thread-id");
|
|
58
|
+
if (!values["body-file"]) throw parseError("Missing required option: --body-file");
|
|
59
|
+
|
|
60
|
+
const repoSlug = values.repo;
|
|
61
|
+
parseRepoSlug(repoSlug);
|
|
62
|
+
const pr = parsePositiveInteger(values.pr, "--pr", parseError);
|
|
63
|
+
const commentId = parsePositiveInteger(values["comment-id"], "--comment-id", parseError);
|
|
64
|
+
|
|
65
|
+
return { repo: repoSlug, pr, commentId, threadId: values["thread-id"], bodyFile: values["body-file"] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function run(argv) {
|
|
69
|
+
const parsed = parseCliArgs(argv);
|
|
70
|
+
if (parsed.help) {
|
|
71
|
+
process.stdout.write(`${USAGE}\n`);
|
|
38
72
|
return 0;
|
|
39
|
-
}
|
|
40
|
-
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { repo: repoSlug, pr, commentId, threadId, bodyFile } = parsed;
|
|
76
|
+
const rawBody = await readFile(bodyFile, "utf8");
|
|
77
|
+
if (rawBody.trim().length === 0) throw new Error("--body-file must contain non-empty text");
|
|
78
|
+
validateResolutionMessage(rawBody);
|
|
79
|
+
|
|
80
|
+
const result = await replyAndMaybeResolve(
|
|
81
|
+
{ repo: repoSlug, pr, commentId, threadId, body: rawBody, resolve: true },
|
|
82
|
+
{ env: process.env, ghCommand: "gh" },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
process.stdout.write(JSON.stringify({
|
|
86
|
+
ok: true, repo: repoSlug, pr, commentId, threadId,
|
|
87
|
+
replyId: result.replyId, replyUrl: result.replyUrl, resolved: true,
|
|
88
|
+
}) + "\n");
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
41
91
|
|
|
42
|
-
if (isDirectCliRun(import.meta.url)) {
|
|
92
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
93
|
+
run(process.argv.slice(2)).then(
|
|
94
|
+
(code) => { process.exitCode = typeof code === "number" ? code : 0; },
|
|
95
|
+
(error) => {
|
|
96
|
+
const usage = error instanceof Error && typeof error.usage === "string" ? error.usage : undefined;
|
|
97
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error), ...(usage && { usage }) })}\n`);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { isDirectCliRun } from "@dev-loops/core/cli/helpers";
|
|
5
7
|
|
|
6
8
|
const CHECKPOINT_FILE = ".pi/dev-loop-retrospective-checkpoint.json";
|
|
7
9
|
const ALLOWED_STATES = new Set(["required", "complete", "skipped", "none", "missing"]);
|
|
8
10
|
|
|
11
|
+
const USAGE = `Usage: dev-loops checkpoint-contract --state <state> [--notes <text>] [--reason <text>]
|
|
12
|
+
|
|
13
|
+
Write .pi/dev-loop-retrospective-checkpoint.json using the retrospective contract format.
|
|
14
|
+
|
|
15
|
+
Required:
|
|
16
|
+
--state <state> Checkpoint state (required, complete, skipped, none, missing)
|
|
17
|
+
|
|
18
|
+
Optional:
|
|
19
|
+
--notes <text> Required when --state is complete
|
|
20
|
+
--reason <text> Required when --state is skipped`;
|
|
21
|
+
|
|
22
|
+
function parseError(message) {
|
|
23
|
+
return Object.assign(new Error(message), { usage: USAGE });
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
export function buildRetrospectiveCheckpointPayload({ state, notes = null, reason = null }, now = new Date()) {
|
|
10
27
|
const timestamp = now.toISOString();
|
|
11
28
|
if (state === "complete") return { state, completedAt: timestamp, notes };
|
|
@@ -16,34 +33,66 @@ export function buildRetrospectiveCheckpointPayload({ state, notes = null, reaso
|
|
|
16
33
|
throw new Error(`Unsupported state: ${state}`);
|
|
17
34
|
}
|
|
18
35
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
function parseCliArgs(argv) {
|
|
37
|
+
let values;
|
|
38
|
+
try {
|
|
39
|
+
({ values } = parseArgs({
|
|
40
|
+
args: argv,
|
|
41
|
+
options: {
|
|
42
|
+
state: { type: "string" },
|
|
43
|
+
notes: { type: "string" },
|
|
44
|
+
reason: { type: "string" },
|
|
45
|
+
help: { type: "boolean", short: "h" },
|
|
46
|
+
},
|
|
47
|
+
strict: true,
|
|
48
|
+
allowPositionals: false,
|
|
49
|
+
}));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
throw parseError(err instanceof Error ? err.message : String(err));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (values.help) {
|
|
55
|
+
return { help: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!values.state) throw parseError("Missing required option: --state");
|
|
59
|
+
const state = values.state;
|
|
60
|
+
if (!ALLOWED_STATES.has(state)) {
|
|
61
|
+
throw parseError(`--state must be one of: ${[...ALLOWED_STATES].join(", ")}`);
|
|
62
|
+
}
|
|
63
|
+
if (state === "complete" && !values.notes) {
|
|
64
|
+
throw parseError('state "complete" requires --notes');
|
|
65
|
+
}
|
|
66
|
+
if (state === "skipped" && !values.reason) {
|
|
67
|
+
throw parseError('state "skipped" requires --reason');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { state, notes: values.notes ?? null, reason: values.reason ?? null };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function run(argv) {
|
|
74
|
+
const parsed = parseCliArgs(argv);
|
|
75
|
+
if (parsed.help) {
|
|
76
|
+
process.stdout.write(`${USAGE}\n`);
|
|
45
77
|
return 0;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
78
|
+
}
|
|
48
79
|
|
|
49
|
-
|
|
80
|
+
const { state, notes, reason } = parsed;
|
|
81
|
+
const payload = buildRetrospectiveCheckpointPayload({ state, notes, reason });
|
|
82
|
+
const checkpointPath = path.join(process.cwd(), CHECKPOINT_FILE);
|
|
83
|
+
await mkdir(path.dirname(checkpointPath), { recursive: true });
|
|
84
|
+
await writeFile(checkpointPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
85
|
+
process.stdout.write(JSON.stringify({ ok: true, path: CHECKPOINT_FILE, checkpoint: payload }) + "\n");
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
90
|
+
run(process.argv.slice(2)).then(
|
|
91
|
+
(code) => { process.exitCode = typeof code === "number" ? code : 0; },
|
|
92
|
+
(error) => {
|
|
93
|
+
const usage = error instanceof Error && typeof error.usage === "string" ? error.usage : undefined;
|
|
94
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error), ...(usage && { usage }) })}\n`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
ASYNC_START_STATUS,
|
|
23
23
|
buildAsyncStartRejection,
|
|
24
24
|
validateAsyncStartContext,
|
|
25
|
+
resolveEffectiveAsyncStartMode,
|
|
25
26
|
} from "@dev-loops/core/loop/async-start-contract";
|
|
26
27
|
import { loadDevLoopConfig, resolveConductorModel, resolveAutonomyStopAt, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
27
28
|
const USAGE = `Usage: outer-loop.mjs --repo <owner/name> --pr <number>
|
|
@@ -286,9 +287,11 @@ export async function runOuterLoop(options, { env = process.env, ghCommand = "gh
|
|
|
286
287
|
devLoopConfig = loaded.config;
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
|
-
const
|
|
290
|
+
const configuredAsyncStartMode = devLoopConfig === null
|
|
290
291
|
? "required"
|
|
291
292
|
: resolveWorkflowConfig(devLoopConfig, "asyncStartMode");
|
|
293
|
+
// Relaxed to "allowed" under the Claude harness (Pi async-start contract does not apply).
|
|
294
|
+
const asyncStartMode = resolveEffectiveAsyncStartMode(configuredAsyncStartMode, env);
|
|
292
295
|
const asyncStartValidation = validateAsyncStartContext({ env, isSnapshotMode, asyncStartMode });
|
|
293
296
|
if (asyncStartValidation.status === ASYNC_START_STATUS.REJECTED) {
|
|
294
297
|
return buildAsyncStartRejection(asyncStartValidation);
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
validateAsyncStartContext,
|
|
18
18
|
buildAsyncStartRejection,
|
|
19
|
+
resolveEffectiveAsyncStartMode,
|
|
19
20
|
ASYNC_START_STATUS,
|
|
20
21
|
} from "@dev-loops/core/loop/async-start-contract";
|
|
21
22
|
import { detectRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
@@ -411,7 +412,10 @@ export function buildResolveDevLoopStartupResult(input, { adapter = createPiAdap
|
|
|
411
412
|
? (STRATEGY_ASYNC_DISPATCH[bundle.selectedStrategy] ?? false)
|
|
412
413
|
: false;
|
|
413
414
|
if (requiresAsyncDispatch) {
|
|
414
|
-
|
|
415
|
+
// The configured async-start mode is relaxed to "allowed" under the Claude
|
|
416
|
+
// harness, where the Pi async-subagent start contract does not apply.
|
|
417
|
+
const effectiveAsyncStartMode = resolveEffectiveAsyncStartMode(asyncStartMode, effectiveEnv);
|
|
418
|
+
const validation = validateAsyncStartContext({ env: effectiveEnv, asyncStartMode: effectiveAsyncStartMode });
|
|
415
419
|
if (validation.status === ASYNC_START_STATUS.REJECTED) {
|
|
416
420
|
return buildAsyncStartRejection(validation);
|
|
417
421
|
}
|
package/scripts/repo-wiki.mjs
CHANGED
|
@@ -1,42 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Primary repo-wiki wrapper. Proxies to the published @mfittko/repo-wiki
|
|
3
|
-
//
|
|
2
|
+
// Primary repo-wiki wrapper. Proxies to either the published @mfittko/repo-wiki
|
|
3
|
+
// npm package (default) or a pinned local source checkout when --source local or
|
|
4
|
+
// REPO_WIKI_SOURCE=local is set.
|
|
4
5
|
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { existsSync } from "node:fs";
|
|
7
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
8
|
import path from "node:path";
|
|
7
9
|
import process from "node:process";
|
|
8
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
9
12
|
|
|
10
13
|
import { isDirectCliRun } from "@dev-loops/core/cli/helpers";
|
|
11
14
|
|
|
12
15
|
// Pinned to the latest published release at the time this slice was opened.
|
|
13
|
-
// Bump deliberately when the consumer repo wants to adopt a newer release.
|
|
14
16
|
export const REPO_WIKI_NPM_PACKAGE = "@mfittko/repo-wiki";
|
|
15
17
|
export const REPO_WIKI_NPM_VERSION = "0.2.6";
|
|
18
|
+
export const REPO_WIKI_MIN_NODE_MAJOR = 20;
|
|
19
|
+
|
|
20
|
+
export const REPO_WIKI_GIT_URL = "https://github.com/mfittko/repo-wiki.git";
|
|
21
|
+
export const REPO_WIKI_REF = "d7e772e3d702a75896a6f4eec574a4e4e5bfa6dd";
|
|
22
|
+
export const REPO_WIKI_LOCAL_MIN_NODE_MAJOR = 24;
|
|
16
23
|
|
|
17
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
25
|
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
19
26
|
|
|
20
|
-
export const
|
|
27
|
+
export const REPO_WIKI_LOCAL_CONFIG_PATH = resolveRepoWikiConfigPath();
|
|
28
|
+
export const REPO_WIKI_SCHEMA_PATH = path.join(PROJECT_ROOT, ".llmwiki", "schema.md");
|
|
21
29
|
|
|
22
30
|
export function resolveRepoWikiConfigPath(projectRoot = PROJECT_ROOT) {
|
|
23
31
|
return path.join(projectRoot, ".llmwiki", "config.json");
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
export const REPO_WIKI_CONFIG_PATH = resolveRepoWikiConfigPath();
|
|
27
|
-
export const REPO_WIKI_SCHEMA_PATH = path.join(PROJECT_ROOT, ".llmwiki", "schema.md");
|
|
28
|
-
|
|
29
|
-
export function parseCliArgs(argv) {
|
|
30
|
-
const args = Array.isArray(argv) ? [...argv] : [];
|
|
31
|
-
if (args.length === 0) {
|
|
32
|
-
return { passthroughArgs: ["--help"] };
|
|
33
|
-
}
|
|
34
|
-
if (args[0] === "--help" || args[0] === "-h") {
|
|
35
|
-
return { passthroughArgs: ["--help"] };
|
|
36
|
-
}
|
|
37
|
-
return { passthroughArgs: args };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
34
|
export function assertSupportedNodeVersion(version = process.versions.node) {
|
|
41
35
|
const major = Number.parseInt(String(version).split(".")[0] ?? "", 10);
|
|
42
36
|
if (!Number.isInteger(major) || major < REPO_WIKI_MIN_NODE_MAJOR) {
|
|
@@ -46,15 +40,21 @@ export function assertSupportedNodeVersion(version = process.versions.node) {
|
|
|
46
40
|
}
|
|
47
41
|
}
|
|
48
42
|
|
|
43
|
+
export function assertSupportedLocalNodeVersion(version = process.versions.node) {
|
|
44
|
+
const major = Number.parseInt(String(version).split(".")[0] ?? "", 10);
|
|
45
|
+
if (!Number.isInteger(major) || major < REPO_WIKI_LOCAL_MIN_NODE_MAJOR) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`repo-wiki local helper requires Node.js ${REPO_WIKI_LOCAL_MIN_NODE_MAJOR}+ because repo-wiki itself requires Node.js ${REPO_WIKI_LOCAL_MIN_NODE_MAJOR}+. Current runtime: ${version}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
49
52
|
export function assertConsumerConfigPresent({
|
|
50
|
-
configPath =
|
|
53
|
+
configPath = REPO_WIKI_LOCAL_CONFIG_PATH,
|
|
51
54
|
projectRoot = PROJECT_ROOT,
|
|
52
55
|
} = {}) {
|
|
53
|
-
// When callers override projectRoot without overriding configPath, derive the
|
|
54
|
-
// expected config path from the override so tests and other call sites stay
|
|
55
|
-
// consistent with the per-project layout.
|
|
56
56
|
const resolvedConfigPath =
|
|
57
|
-
configPath ===
|
|
57
|
+
configPath === REPO_WIKI_LOCAL_CONFIG_PATH ? resolveRepoWikiConfigPath(projectRoot) : configPath;
|
|
58
58
|
if (!existsSync(resolvedConfigPath)) {
|
|
59
59
|
throw new Error(
|
|
60
60
|
`Missing required repo-wiki config at ${path.relative(projectRoot, resolvedConfigPath) || resolvedConfigPath}.\n` +
|
|
@@ -73,38 +73,185 @@ export function buildNpxInvocation({
|
|
|
73
73
|
return ["npx", "--yes", `${packageName}@${version}`, ...passthroughArgs];
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
function run(command, args, options = {}) {
|
|
77
|
+
const result = spawnSync(command, args, {
|
|
78
|
+
cwd: options.cwd ?? PROJECT_ROOT,
|
|
79
|
+
env: { ...process.env, ...(options.env ?? {}) },
|
|
80
|
+
stdio: options.stdio ?? "inherit",
|
|
81
|
+
encoding: options.encoding ?? "utf8",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.error) {
|
|
85
|
+
throw result.error;
|
|
86
|
+
}
|
|
87
|
+
if (result.status !== 0 && !options.allowNonZero) {
|
|
88
|
+
const printable = [command, ...args].join(" ");
|
|
89
|
+
throw new Error(`Command failed (${result.status ?? "unknown"}): ${printable}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function runNpxInvocation({
|
|
77
96
|
command,
|
|
78
97
|
args,
|
|
79
98
|
cwd = PROJECT_ROOT,
|
|
80
99
|
env = process.env,
|
|
81
100
|
} = {}) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
return run(command, args, { cwd, env, stdio: "inherit", allowNonZero: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const VALID_REPO_WIKI_SOURCES = new Set(["npm", "local"]);
|
|
105
|
+
|
|
106
|
+
export function parseCliArgs(argv) {
|
|
107
|
+
const args = Array.isArray(argv) ? [...argv] : [];
|
|
108
|
+
const { values } = parseArgs({
|
|
109
|
+
args,
|
|
110
|
+
options: {
|
|
111
|
+
source: { type: "string" },
|
|
112
|
+
},
|
|
113
|
+
strict: false,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const sourceEnv = process.env.REPO_WIKI_SOURCE;
|
|
117
|
+
const source = values.source || sourceEnv || "npm";
|
|
118
|
+
if (!VALID_REPO_WIKI_SOURCES.has(source)) {
|
|
119
|
+
throw new Error(`Unknown repo-wiki source "${source}". Valid sources: npm, local`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const passthroughArgs = [];
|
|
123
|
+
for (let i = 0; i < args.length; i++) {
|
|
124
|
+
const a = args[i];
|
|
125
|
+
if (a === "--source") {
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (a.startsWith("--source=")) continue;
|
|
130
|
+
passthroughArgs.push(a);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (passthroughArgs.length === 0 || passthroughArgs[0] === "help" || passthroughArgs[0] === "--help" || passthroughArgs[0] === "-h") {
|
|
134
|
+
return { source, prepareOnly: false, passthroughArgs: ["--help"] };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (passthroughArgs[0] === "prepare") {
|
|
138
|
+
return { source, prepareOnly: true, passthroughArgs: [] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { source, prepareOnly: false, passthroughArgs };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function resolveRepoWikiPaths(projectRoot = PROJECT_ROOT, ref = REPO_WIKI_REF) {
|
|
145
|
+
const baseDir = path.join(projectRoot, ".tmp", "repo-wiki", ref);
|
|
146
|
+
const sourceDir = path.join(baseDir, "source");
|
|
147
|
+
const cliPath = path.join(sourceDir, "dist", "bin", "repo-wiki.js");
|
|
148
|
+
const buildStampPath = path.join(baseDir, "build-stamp.json");
|
|
149
|
+
return { projectRoot, baseDir, sourceDir, cliPath, buildStampPath };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function readBuildStamp(buildStampPath) {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(await readFile(buildStampPath, "utf8"));
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function writeBuildStamp(buildStampPath) {
|
|
161
|
+
await writeFile(buildStampPath, JSON.stringify({ ref: REPO_WIKI_REF }, null, 2) + "\n", "utf8");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function ensureRepoWikiPrepared(projectRoot = PROJECT_ROOT) {
|
|
165
|
+
assertSupportedLocalNodeVersion();
|
|
166
|
+
const { baseDir, sourceDir, cliPath, buildStampPath } = resolveRepoWikiPaths(projectRoot);
|
|
167
|
+
await mkdir(baseDir, { recursive: true });
|
|
168
|
+
|
|
169
|
+
let currentHead = null;
|
|
170
|
+
try {
|
|
171
|
+
run("git", ["-C", sourceDir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
|
|
172
|
+
currentHead = run("git", ["-C", sourceDir, "rev-parse", "HEAD"], { stdio: "pipe" }).stdout.trim();
|
|
173
|
+
} catch {
|
|
174
|
+
run("git", ["clone", REPO_WIKI_GIT_URL, sourceDir], { cwd: baseDir });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (currentHead !== REPO_WIKI_REF) {
|
|
178
|
+
run("git", ["-C", sourceDir, "fetch", "origin", REPO_WIKI_REF, "--depth", "1"]);
|
|
179
|
+
run("git", ["-C", sourceDir, "checkout", "--force", REPO_WIKI_REF]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const stamp = await readBuildStamp(buildStampPath);
|
|
183
|
+
if (stamp?.ref !== REPO_WIKI_REF) {
|
|
184
|
+
run("npm", ["install", "--silent"], { cwd: sourceDir });
|
|
185
|
+
run("npm", ["run", "build", "--silent"], { cwd: sourceDir });
|
|
186
|
+
await writeBuildStamp(buildStampPath);
|
|
187
|
+
} else {
|
|
188
|
+
try {
|
|
189
|
+
run(process.execPath, [cliPath, "--help"], { stdio: "ignore" });
|
|
190
|
+
} catch {
|
|
191
|
+
run("npm", ["install", "--silent"], { cwd: sourceDir });
|
|
192
|
+
run("npm", ["run", "build", "--silent"], { cwd: sourceDir });
|
|
193
|
+
await writeBuildStamp(buildStampPath);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return resolveRepoWikiPaths(projectRoot);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function runRepoWikiNpm(passthroughArgs, projectRoot) {
|
|
201
|
+
assertSupportedNodeVersion();
|
|
202
|
+
assertConsumerConfigPresent({ projectRoot });
|
|
203
|
+
const invocation = buildNpxInvocation({ passthroughArgs });
|
|
204
|
+
const result = runNpxInvocation({ command: invocation[0], args: invocation.slice(1), cwd: projectRoot });
|
|
205
|
+
|
|
206
|
+
if (result.status !== 0) {
|
|
207
|
+
return { ok: false, status: result.status ?? 1, source: "npm", invocation };
|
|
208
|
+
}
|
|
209
|
+
return { ok: true, status: 0, source: "npm", invocation };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function _runRepoWikiLocal(passthroughArgs, projectRoot) {
|
|
213
|
+
const { cliPath } = await ensureRepoWikiPrepared(projectRoot);
|
|
214
|
+
const result = spawnSync(process.execPath, [cliPath, ...passthroughArgs], {
|
|
215
|
+
cwd: projectRoot,
|
|
216
|
+
env: process.env,
|
|
85
217
|
stdio: "inherit",
|
|
86
218
|
});
|
|
87
219
|
if (result.error) {
|
|
88
220
|
throw result.error;
|
|
89
221
|
}
|
|
90
|
-
|
|
222
|
+
if (result.status !== 0) {
|
|
223
|
+
return { ok: false, status: result.status ?? 1, source: "local", cliPath };
|
|
224
|
+
}
|
|
225
|
+
return { ok: true, status: 0, source: "local", cliPath };
|
|
91
226
|
}
|
|
92
227
|
|
|
93
228
|
// Composable entry point: returns a structured result instead of calling
|
|
94
229
|
// process.exit so importers can reuse this wrapper without terminating the host
|
|
95
230
|
// process. The direct-run block below owns the process-level exit-code mapping.
|
|
231
|
+
// Convenience export for callers that explicitly want the pinned source path.
|
|
232
|
+
export async function runRepoWikiLocal(argv, projectRoot = PROJECT_ROOT) {
|
|
233
|
+
const args = argv.some((a) => a === "--source" || a.startsWith("--source="))
|
|
234
|
+
? argv
|
|
235
|
+
: ["--source", "local", ...argv];
|
|
236
|
+
return runRepoWiki(args, projectRoot);
|
|
237
|
+
}
|
|
238
|
+
|
|
96
239
|
export async function runRepoWiki(argv, projectRoot = PROJECT_ROOT) {
|
|
97
|
-
|
|
98
|
-
assertConsumerConfigPresent({ projectRoot });
|
|
99
|
-
const { passthroughArgs } = parseCliArgs(argv);
|
|
100
|
-
const invocation = buildNpxInvocation({ passthroughArgs });
|
|
240
|
+
const { source, prepareOnly, passthroughArgs } = parseCliArgs(argv);
|
|
101
241
|
|
|
102
|
-
|
|
242
|
+
if (source === "local") {
|
|
243
|
+
if (prepareOnly) {
|
|
244
|
+
const { cliPath } = await ensureRepoWikiPrepared(projectRoot);
|
|
245
|
+
return { ok: true, status: 0, source: "local", prepared: true, cliPath };
|
|
246
|
+
}
|
|
247
|
+
return _runRepoWikiLocal(passthroughArgs, projectRoot);
|
|
248
|
+
}
|
|
103
249
|
|
|
104
|
-
if (
|
|
105
|
-
|
|
250
|
+
if (prepareOnly) {
|
|
251
|
+
// prepare is meaningless for the npm source; treat as help.
|
|
252
|
+
return runRepoWikiNpm(["--help"], projectRoot);
|
|
106
253
|
}
|
|
107
|
-
return
|
|
254
|
+
return runRepoWikiNpm(passthroughArgs, projectRoot);
|
|
108
255
|
}
|
|
109
256
|
|
|
110
257
|
if (isDirectCliRun(import.meta.url)) {
|
package/skills/dev-loop/SKILL.md
CHANGED
|
@@ -26,7 +26,7 @@ Required installed runtime contract docs are shared bundled copies under `../doc
|
|
|
26
26
|
|
|
27
27
|
The main agent must **always** dispatch the `dev-loop` async subagent for any dev-loop work.
|
|
28
28
|
Do not run `dev-loops loop startup` or any startup resolver in the main agent.
|
|
29
|
-
|
|
29
|
+
For async-required routes (config `workflow.asyncStartMode`, default `required`) the resolver needs an async run-id marker (`DEVLOOPS_RUN_ID`, or the `PI_SUBAGENT_RUN_ID` alias) that the Pi harness injects when it dispatches the async subagent; under the Claude Code harness the requirement is relaxed automatically (no marker needed). The startup resolver also runs without a marker for non-async routes. Regardless, only the `dev-loop` subagent runs it — never the main agent.
|
|
30
30
|
|
|
31
31
|
### Dev-loop subagent (post-dispatch)
|
|
32
32
|
|
|
@@ -86,7 +86,7 @@ Do not preload route packs before the resolver selects the strategy.
|
|
|
86
86
|
|
|
87
87
|
## Async dispatch
|
|
88
88
|
|
|
89
|
-
**Async dispatch rule (enforced):** the resolver
|
|
89
|
+
**Async dispatch rule (enforced):** the resolver fails closed for GitHub-first strategies when `canonicalStateSummary.requiresAsyncDispatch` is `true` (default `required` mode) — inline invocation without an async run-id marker (`DEVLOOPS_RUN_ID`, or the `PI_SUBAGENT_RUN_ID` alias) is rejected for those routes. Under the Claude Code harness this requirement is relaxed automatically. See [Startup procedure](#startup-procedure).
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
## Fallback gate-comment poster
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Pinned-source fallback repo-wiki wrapper. Clones mfittko/repo-wiki at a fixed
|
|
3
|
-
// commit, installs/builds it locally, and proxies the requested command against
|
|
4
|
-
// this repo.
|
|
5
|
-
//
|
|
6
|
-
// This helper is retained for environments that prefer a pinned source checkout
|
|
7
|
-
// over the published npm install path (deterministic source pin, controlled
|
|
8
|
-
// GitHub-only network access, offline reproduction after the initial clone).
|
|
9
|
-
// It still requires git access to https://github.com/mfittko/repo-wiki.git for
|
|
10
|
-
// the initial clone/fetch step.
|
|
11
|
-
//
|
|
12
|
-
// The primary repo-wiki entrypoint is `scripts/repo-wiki.mjs` (npm-installed).
|
|
13
|
-
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import process from "node:process";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
|
|
19
|
-
import { isDirectCliRun } from "@dev-loops/core/cli/helpers";
|
|
20
|
-
|
|
21
|
-
export const REPO_WIKI_GIT_URL = "https://github.com/mfittko/repo-wiki.git";
|
|
22
|
-
export const REPO_WIKI_REF = "d7e772e3d702a75896a6f4eec574a4e4e5bfa6dd";
|
|
23
|
-
export const REPO_WIKI_MIN_NODE_MAJOR = 24;
|
|
24
|
-
|
|
25
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
27
|
-
|
|
28
|
-
export function parseCliArgs(argv) {
|
|
29
|
-
const args = Array.isArray(argv) ? [...argv] : [];
|
|
30
|
-
if (args.length === 0) {
|
|
31
|
-
return { prepareOnly: false, passthroughArgs: ["--help"] };
|
|
32
|
-
}
|
|
33
|
-
if (args[0] === "prepare") {
|
|
34
|
-
return { prepareOnly: true, passthroughArgs: [] };
|
|
35
|
-
}
|
|
36
|
-
return { prepareOnly: false, passthroughArgs: args };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function resolveRepoWikiPaths(projectRoot = PROJECT_ROOT, ref = REPO_WIKI_REF) {
|
|
40
|
-
const baseDir = path.join(projectRoot, ".tmp", "repo-wiki", ref);
|
|
41
|
-
const sourceDir = path.join(baseDir, "source");
|
|
42
|
-
const cliPath = path.join(sourceDir, "dist", "bin", "repo-wiki.js");
|
|
43
|
-
const buildStampPath = path.join(baseDir, "build-stamp.json");
|
|
44
|
-
return { projectRoot, baseDir, sourceDir, cliPath, buildStampPath };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function assertSupportedNodeVersion(version = process.versions.node) {
|
|
48
|
-
const major = Number.parseInt(String(version).split(".")[0] ?? "", 10);
|
|
49
|
-
if (!Number.isInteger(major) || major < REPO_WIKI_MIN_NODE_MAJOR) {
|
|
50
|
-
throw new Error(
|
|
51
|
-
`repo-wiki local helper requires Node.js ${REPO_WIKI_MIN_NODE_MAJOR}+ because repo-wiki itself requires Node.js ${REPO_WIKI_MIN_NODE_MAJOR}+. Current runtime: ${version}`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function run(command, args, options = {}) {
|
|
57
|
-
const result = spawnSync(command, args, {
|
|
58
|
-
cwd: options.cwd ?? PROJECT_ROOT,
|
|
59
|
-
env: { ...process.env, ...(options.env ?? {}) },
|
|
60
|
-
stdio: options.stdio ?? "inherit",
|
|
61
|
-
encoding: options.encoding ?? "utf8",
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
if (result.error) {
|
|
65
|
-
throw result.error;
|
|
66
|
-
}
|
|
67
|
-
if (result.status !== 0) {
|
|
68
|
-
const printable = [command, ...args].join(" ");
|
|
69
|
-
throw new Error(`Command failed (${result.status ?? "unknown"}): ${printable}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function readBuildStamp(buildStampPath) {
|
|
76
|
-
try {
|
|
77
|
-
return JSON.parse(await readFile(buildStampPath, "utf8"));
|
|
78
|
-
} catch {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function writeBuildStamp(buildStampPath) {
|
|
84
|
-
await writeFile(buildStampPath, JSON.stringify({ ref: REPO_WIKI_REF }, null, 2) + "\n", "utf8");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function ensureRepoWikiPrepared(projectRoot = PROJECT_ROOT) {
|
|
88
|
-
assertSupportedNodeVersion();
|
|
89
|
-
const { baseDir, sourceDir, cliPath, buildStampPath } = resolveRepoWikiPaths(projectRoot);
|
|
90
|
-
await mkdir(baseDir, { recursive: true });
|
|
91
|
-
|
|
92
|
-
let currentHead = null;
|
|
93
|
-
try {
|
|
94
|
-
run("git", ["-C", sourceDir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
|
|
95
|
-
currentHead = run("git", ["-C", sourceDir, "rev-parse", "HEAD"], { stdio: "pipe" }).stdout.trim();
|
|
96
|
-
} catch {
|
|
97
|
-
run("git", ["clone", REPO_WIKI_GIT_URL, sourceDir], { cwd: baseDir });
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (currentHead !== REPO_WIKI_REF) {
|
|
101
|
-
run("git", ["-C", sourceDir, "fetch", "origin", REPO_WIKI_REF, "--depth", "1"]);
|
|
102
|
-
run("git", ["-C", sourceDir, "checkout", "--force", REPO_WIKI_REF]);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const stamp = await readBuildStamp(buildStampPath);
|
|
106
|
-
if (stamp?.ref !== REPO_WIKI_REF) {
|
|
107
|
-
run("npm", ["install", "--silent"], { cwd: sourceDir });
|
|
108
|
-
run("npm", ["run", "build", "--silent"], { cwd: sourceDir });
|
|
109
|
-
await writeBuildStamp(buildStampPath);
|
|
110
|
-
} else {
|
|
111
|
-
try {
|
|
112
|
-
run(process.execPath, [cliPath, "--help"], { stdio: "ignore" });
|
|
113
|
-
} catch {
|
|
114
|
-
run("npm", ["install", "--silent"], { cwd: sourceDir });
|
|
115
|
-
run("npm", ["run", "build", "--silent"], { cwd: sourceDir });
|
|
116
|
-
await writeBuildStamp(buildStampPath);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return resolveRepoWikiPaths(projectRoot);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Composable entry point: returns a structured result instead of calling
|
|
124
|
-
// process.exit so importers can reuse this wrapper without terminating the host
|
|
125
|
-
// process. The direct-run block below owns the process-level exit-code mapping.
|
|
126
|
-
export async function runRepoWikiLocal(argv, projectRoot = PROJECT_ROOT) {
|
|
127
|
-
const { prepareOnly, passthroughArgs } = parseCliArgs(argv);
|
|
128
|
-
const { cliPath } = await ensureRepoWikiPrepared(projectRoot);
|
|
129
|
-
if (prepareOnly) {
|
|
130
|
-
return { ok: true, status: 0, prepared: true, cliPath };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const result = spawnSync(process.execPath, [cliPath, ...passthroughArgs], {
|
|
134
|
-
cwd: projectRoot,
|
|
135
|
-
env: process.env,
|
|
136
|
-
stdio: "inherit",
|
|
137
|
-
});
|
|
138
|
-
if (result.error) {
|
|
139
|
-
throw result.error;
|
|
140
|
-
}
|
|
141
|
-
if (result.status !== 0) {
|
|
142
|
-
return { ok: false, status: result.status ?? 1, cliPath };
|
|
143
|
-
}
|
|
144
|
-
return { ok: true, status: 0, prepared: true, cliPath };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (isDirectCliRun(import.meta.url)) {
|
|
148
|
-
runRepoWikiLocal(process.argv.slice(2)).then((result) => {
|
|
149
|
-
if (!result.ok) {
|
|
150
|
-
process.exitCode = result.status;
|
|
151
|
-
}
|
|
152
|
-
}).catch((error) => {
|
|
153
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
154
|
-
process.exitCode = 1;
|
|
155
|
-
});
|
|
156
|
-
}
|