dev-loops 0.1.3 → 0.2.1
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 +13 -0
- package/.claude/agents/dev-loop.md +69 -0
- package/.claude/agents/developer.md +35 -0
- package/.claude/agents/docs.md +31 -0
- package/.claude/agents/fixer.md +51 -0
- package/.claude/agents/quality.md +26 -0
- package/.claude/agents/refiner.md +85 -0
- package/.claude/agents/review.md +61 -0
- package/.claude/hooks/_hook-io.mjs +38 -0
- package/.claude/hooks/hooks.json +35 -0
- package/.claude/hooks/post-tool-use-merge.mjs +23 -0
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +73 -0
- package/.claude/hooks/pre-tool-use-write-guard.mjs +55 -0
- package/.claude/skills/copilot-pr-followup/SKILL.md +376 -0
- package/.claude/skills/dev-loop/SKILL.md +140 -0
- package/.claude/skills/dev-loop/templates/bootstrap-agents.md +26 -0
- package/.claude/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
- package/.claude/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
- package/.claude/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
- package/.claude/skills/dev-loop/templates/dev-mode-review.md +17 -0
- package/.claude/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
- package/.claude/skills/dev-loop/templates/merged-phase-plan.md +19 -0
- package/.claude/skills/dev-loop/templates/phase-doc.md +27 -0
- package/.claude/skills/dev-loop/templates/phase-summary.md +13 -0
- package/.claude/skills/dev-loop/templates/phase-variant.md +15 -0
- package/.claude/skills/dev-loop/templates/retrospective.md +11 -0
- package/.claude/skills/dev-loop/templates/review.md +32 -0
- package/.claude/skills/dev-loop/templates/ui-vision-review.md +55 -0
- package/.claude/skills/docs/acceptance-criteria-verification.md +21 -0
- package/.claude/skills/docs/anti-patterns.md +21 -0
- package/.claude/skills/docs/artifact-authority-contract.md +119 -0
- package/.claude/skills/docs/confirmation-rules.md +28 -0
- package/.claude/skills/docs/copilot-ci-status-contract.md +52 -0
- package/.claude/skills/docs/copilot-loop-operations.md +233 -0
- package/.claude/skills/docs/debt-remediation-contract.md +107 -0
- package/.claude/skills/docs/entrypoint-strategies.md +115 -0
- package/.claude/skills/docs/epic-tree-refinement-procedure.md +234 -0
- package/.claude/skills/docs/issue-intake-procedure.md +235 -0
- package/.claude/skills/docs/main-agent-contract.md +85 -0
- package/.claude/skills/docs/merge-preconditions.md +29 -0
- package/.claude/skills/docs/pr-lifecycle-contract.md +209 -0
- package/.claude/skills/docs/public-dev-loop-contract.md +497 -0
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +159 -0
- package/.claude/skills/docs/stop-conditions.md +29 -0
- package/.claude/skills/docs/structural-quality.md +42 -0
- package/.claude/skills/docs/tracker-first-loop-state.md +281 -0
- package/.claude/skills/docs/validation-policy.md +27 -0
- package/.claude/skills/docs/workflow-handoff-contract.md +135 -0
- package/.claude/skills/final-approval/SKILL.md +18 -0
- package/.claude/skills/local-implementation/SKILL.md +636 -0
- package/.claude-plugin/marketplace.json +19 -0
- package/CHANGELOG.md +70 -0
- package/README.md +28 -0
- package/agents/dev-loop.agent.md +4 -0
- package/cli/index.mjs +5 -5
- package/extension/checks.ts +18 -29
- package/extension/harness-types.ts +47 -0
- package/extension/index.ts +10 -8
- package/extension/pi-extension-adapter.ts +64 -0
- package/extension/post-merge-update.ts +69 -190
- package/package.json +17 -9
- package/scripts/README.md +6 -3
- package/scripts/claude/generate-claude-assets.mjs +175 -0
- package/scripts/claude/headless-dev-loop.mjs +89 -0
- package/scripts/claude/headless-info-smoke.mjs +90 -0
- package/scripts/github/_review-thread-mutations.mjs +3 -0
- package/scripts/github/reply-resolve-review-threads.mjs +3 -3
- package/scripts/github/upsert-checkpoint-verdict.mjs +4 -3
- package/scripts/loop/_pr-runner-coordination.mjs +2 -1
- package/scripts/loop/copilot-pr-handoff.mjs +3 -2
- package/scripts/loop/detect-stale-runner.mjs +5 -7
- package/scripts/loop/info.mjs +3 -2
- package/scripts/loop/inspect-run-viewer/server.mjs +2 -1
- package/scripts/loop/outer-loop.mjs +2 -2
- package/scripts/loop/pr-runner-coordination.mjs +4 -4
- package/skills/docs/main-agent-contract.md +19 -6
|
@@ -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,76 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.2.1
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Claude Code marketplace catalog** (#828): ship `.claude-plugin/marketplace.json` at the repo
|
|
10
|
+
root so the repo can be added as a plugin marketplace (`/plugin marketplace add mfittko/dev-loops`,
|
|
11
|
+
or the *Manage Plugins → Marketplaces → Add* UI) and the plugin installed with
|
|
12
|
+
`/plugin install dev-loops@dev-loops`. The catalog's single plugin entry sources the existing
|
|
13
|
+
in-repo plugin at `./.claude`; the plugin version stays authoritative in `plugin.json`. A
|
|
14
|
+
contract test locks the catalog shape, and `.claude-plugin/` is added to the npm `files`
|
|
15
|
+
allowlist. Verified end-to-end with `claude plugin validate` + `marketplace add`/`install`
|
|
16
|
+
(4 skills, 7 agents, 2 hooks).
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `plugin.json` now declares an `author` (clears the marketplace-validation warning).
|
|
21
|
+
- README "Claude Code plugin" section drops the `(preview)` framing and documents marketplace
|
|
22
|
+
install; the two CLI help lines that said plugin packaging was "in progress" are updated.
|
|
23
|
+
|
|
24
|
+
## 0.2.0
|
|
25
|
+
|
|
26
|
+
### Added — Claude Code harness (agent-harness-agnostic dev-loop)
|
|
27
|
+
|
|
28
|
+
dev-loops is now dual-harness: it runs under both Pi and Claude Code. Pi behavior is unchanged.
|
|
29
|
+
|
|
30
|
+
- **Harness adapter seam** (#770): a neutral `ExtensionHarnessAdapter` (exec + lifecycle +
|
|
31
|
+
command registration + ui) with Pi and Claude adapters; `@dev-loops/core/harness`.
|
|
32
|
+
- **Neutral run-id contract** (#771): `DEVLOOPS_RUN_ID` (with `PI_SUBAGENT_RUN_ID` as a
|
|
33
|
+
backward-compatible alias) via `@dev-loops/core/loop/run-context`; all runner-coordination /
|
|
34
|
+
async-start readers route through it.
|
|
35
|
+
- **Generated `.claude` assets** (#772, #816, #817): a deterministic generator emits
|
|
36
|
+
`.claude/agents` + `.claude/skills` from the canonical Pi sources (`@dev-loops/core/claude/
|
|
37
|
+
asset-generation`), with the Pi→Claude tool-name mapping, bundled shared contract docs +
|
|
38
|
+
templates, and Pi-runtime-only prose stripped via `<!-- pi-only -->` markers.
|
|
39
|
+
- **Claude hooks + read-only enforcement** (#773): PreToolUse Bash draft-gate guard + Write/Edit
|
|
40
|
+
main-agent read-only guard (`@dev-loops/core/claude/hook-decisions`), opt-in via
|
|
41
|
+
`DEVLOOPS_MAIN_AGENT_READONLY`.
|
|
42
|
+
- **CLI Pi-neutrality** (#774): `npx dev-loops --help`/`status` run with no `@earendil-works/pi-*`
|
|
43
|
+
present; Pi-only install strings no longer shown unconditionally.
|
|
44
|
+
- **Headless entry** (#775): a `claude -p` headless dev-loop entry (`@dev-loops/core/claude/
|
|
45
|
+
headless-entry`) that mints + propagates the run id, plus an offline read-only CI/Docker smoke
|
|
46
|
+
(`npm run smoke:headless`); the Pi Docker smoke is preserved (dual-harness).
|
|
47
|
+
- **Claude Code plugin** (#818, #824): `.claude/.claude-plugin/plugin.json` (plugin root
|
|
48
|
+
`.claude/`) bundling the dev-loop agents, skills, and hooks —
|
|
49
|
+
`claude --plugin-dir .claude` loads 4 skills, 7 agents, 2 hooks.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- `@dev-loops/core` bumped to `^0.2.0` (new `claude/*`, `loop/run-context`, and
|
|
54
|
+
`loop/bash-command-classify` exports).
|
|
55
|
+
|
|
56
|
+
## 0.1.3
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- Removed a stale `defaults.yaml` from the `files` allowlist and regenerated the lockfile (#806).
|
|
61
|
+
|
|
62
|
+
## 0.1.2
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- Ship the extension-packaged dev-loop defaults only; removed the duplicated
|
|
67
|
+
`.pi/dev-loop/defaults.yaml` (#805).
|
|
68
|
+
|
|
69
|
+
## 0.1.1
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
- Renamed the Pi peer dependencies to the `@earendil-works/pi-*` scope (#799).
|
|
74
|
+
|
|
5
75
|
## 0.1.0
|
|
6
76
|
|
|
7
77
|
### Added
|
package/README.md
CHANGED
|
@@ -45,6 +45,34 @@ Or run directly without installing:
|
|
|
45
45
|
npx dev-loops --help
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
### Claude Code plugin
|
|
49
|
+
|
|
50
|
+
The repo ships a Claude Code plugin rooted at `.claude/` (manifest at
|
|
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:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
claude --plugin-dir .claude # load it for a session
|
|
65
|
+
claude --plugin-dir .claude plugin details dev-loops # inspect the discovered components
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
When installed from npm, point at the bundled copy: `claude --plugin-dir node_modules/dev-loops/.claude`.
|
|
69
|
+
|
|
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.
|
|
75
|
+
|
|
48
76
|
### Pi extension
|
|
49
77
|
|
|
50
78
|
To use `/dev-loops` inside Pi:
|
package/agents/dev-loop.agent.md
CHANGED
|
@@ -59,7 +59,9 @@ If local facts, GitHub facts, and helper/state-machine output do not agree well
|
|
|
59
59
|
|
|
60
60
|
## Subagent delegation
|
|
61
61
|
|
|
62
|
+
<!-- pi-only -->
|
|
62
63
|
This agent has `tools: [subagent]` and `maxSubagentDepth: 3` to allow orchestrating parallel review, chains, and staged fix passes.
|
|
64
|
+
<!-- /pi-only -->
|
|
63
65
|
|
|
64
66
|
All delegation must originate from the handoff envelope: the envelope's `nextAction`, `requiredReads`, `stopRules`, and `acceptance` define the bounded task. The envelope is passed to child subagents as their primary handoff artifact.
|
|
65
67
|
|
|
@@ -68,12 +70,14 @@ The pi-subagents skill is parent-only, so delegated subagents do not receive orc
|
|
|
68
70
|
- No child subagent spawning beyond assigned fanout work.
|
|
69
71
|
- Bounded tasks with concrete scope, exit conditions, and validation expectations.
|
|
70
72
|
|
|
73
|
+
<!-- pi-only -->
|
|
71
74
|
**Supervisor communication (known pi runtime bug #671):** The pi runtime `contact_supervisor` tool has a broken response path — supervisor responses do not flow back to resolve the pending subagent tool call. Subagents calling `contact_supervisor` become blocked until the idle timeout fires (~60s), then pause without the decision.
|
|
72
75
|
|
|
73
76
|
- **Prefer `intercom` when available.** If the `pi-intercom` extension is active, use `intercom({ action: "ask", ... })` instead of `contact_supervisor`. The `intercom` tool uses message-based delivery (no blocking tool-call state) — see the pi documentation for `intercom({ action: "ask", ... })` parameters and reply conventions.
|
|
74
77
|
- **When `intercom` is unavailable,** do not call `contact_supervisor`. Instead, brief the supervisor to include the decision in the resume message when re-dispatching. The subagent states what it needs in the task description; the supervisor provides the answer on resume. This avoids the broken response path entirely.
|
|
75
78
|
- **If `contact_supervisor` was already called** (legacy code or unavoidable): expect a ~60s idle timeout followed by a pause. On resume, the supervisor must inject the decision in the resume message — do not rely on `intercom` on resume when it was unavailable at call time.
|
|
76
79
|
- **Timeout detection (supervisor-side):** if a `contact_supervisor` call has been pending for >30s, the supervisor should treat it as a probable timeout and prepare to inject the decision in the resume message on re-dispatch. The subagent cannot execute this detection while blocked inside `contact_supervisor`; the supervisor must observe the pending duration externally.
|
|
80
|
+
<!-- /pi-only -->
|
|
77
81
|
|
|
78
82
|
## Output
|
|
79
83
|
|
package/cli/index.mjs
CHANGED
|
@@ -195,7 +195,7 @@ function buildCliHelpLines() {
|
|
|
195
195
|
"dev-loops help",
|
|
196
196
|
"",
|
|
197
197
|
"Workflow entry:",
|
|
198
|
-
"-
|
|
198
|
+
"- `/dev-loop` (Claude Code) or `/skill:dev-loop` (Pi) — single public entrypoint; routing handles the rest",
|
|
199
199
|
"",
|
|
200
200
|
"Commands:",
|
|
201
201
|
"- dev-loops help Show this help",
|
|
@@ -209,8 +209,8 @@ function buildCliHelpLines() {
|
|
|
209
209
|
"Use `dev-loops <category> <subcommand> --help` for per-subcommand usage.",
|
|
210
210
|
"",
|
|
211
211
|
"`/dev-loops hide` remains an extension-only Pi command.",
|
|
212
|
-
"
|
|
213
|
-
"
|
|
212
|
+
"Run via `npx dev-loops` (or `npm install -g dev-loops` for the shell command); see the",
|
|
213
|
+
"README for Pi-extension and Claude Code plugin setup.",
|
|
214
214
|
];
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -230,9 +230,9 @@ function orderedCliSetupSteps(checks) {
|
|
|
230
230
|
const steps = [...new Set(DEV_LOOP_CHECK_IDS.filter((id) => byId.get(id)?.ok === false).map((id) => CLI_SETUP_GUIDANCE[id]))];
|
|
231
231
|
if (steps.length > 0) return steps.map((step, i) => `${i + 1}. ${step}`);
|
|
232
232
|
return [
|
|
233
|
-
"1. Use `/
|
|
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.
|
|
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/extension/checks.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
|
|
3
1
|
import { collectDevLoopChecks as collectSharedDevLoopChecks, DEV_LOOP_CHECK_IDS } from '../lib/dev-loops-core.mjs';
|
|
4
2
|
import { createInspectRunViewerLifecycleManager } from '../scripts/loop/inspect-run-viewer/managed-instance.mjs';
|
|
3
|
+
import type { ExtensionHarnessAdapter } from './harness-types.ts';
|
|
5
4
|
|
|
6
5
|
export type DevLoopCheckId = (typeof DEV_LOOP_CHECK_IDS)[number];
|
|
7
6
|
|
|
@@ -12,47 +11,37 @@ export type DevLoopCheck = {
|
|
|
12
11
|
detail: string;
|
|
13
12
|
};
|
|
14
13
|
|
|
15
|
-
async function commandExists(
|
|
14
|
+
async function commandExists(adapter: ExtensionHarnessAdapter, command: string): Promise<boolean> {
|
|
16
15
|
try {
|
|
17
|
-
const result = await
|
|
18
|
-
timeout: 5_000,
|
|
19
|
-
});
|
|
16
|
+
const result = await adapter.exec(`command -v ${command} >/dev/null 2>&1`, { timeout: 5_000 });
|
|
20
17
|
return result.code === 0;
|
|
21
18
|
} catch {
|
|
22
19
|
return false;
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
async function ghAuthOk(
|
|
23
|
+
async function ghAuthOk(adapter: ExtensionHarnessAdapter): Promise<boolean> {
|
|
27
24
|
try {
|
|
28
|
-
const result = await
|
|
29
|
-
timeout: 10_000,
|
|
30
|
-
});
|
|
25
|
+
const result = await adapter.exec('gh auth status >/dev/null 2>&1', { timeout: 10_000 });
|
|
31
26
|
return result.code === 0;
|
|
32
27
|
} catch {
|
|
33
28
|
return false;
|
|
34
29
|
}
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
async function insideGitRepo(
|
|
32
|
+
async function insideGitRepo(adapter: ExtensionHarnessAdapter): Promise<boolean> {
|
|
38
33
|
try {
|
|
39
|
-
const result = await
|
|
40
|
-
'bash',
|
|
41
|
-
['-lc', 'git rev-parse --is-inside-work-tree >/dev/null 2>&1'],
|
|
42
|
-
{ timeout: 5_000 },
|
|
43
|
-
);
|
|
34
|
+
const result = await adapter.exec('git rev-parse --is-inside-work-tree >/dev/null 2>&1', { timeout: 5_000 });
|
|
44
35
|
return result.code === 0;
|
|
45
36
|
} catch {
|
|
46
37
|
return false;
|
|
47
38
|
}
|
|
48
39
|
}
|
|
49
40
|
|
|
50
|
-
async function getRepoRoot(
|
|
51
|
-
const result = await
|
|
52
|
-
timeout: 10_000,
|
|
53
|
-
});
|
|
41
|
+
async function getRepoRoot(adapter: ExtensionHarnessAdapter): Promise<string> {
|
|
42
|
+
const result = await adapter.exec('git rev-parse --show-toplevel', { timeout: 10_000 });
|
|
54
43
|
if (result.code !== 0) {
|
|
55
|
-
throw new Error('Open
|
|
44
|
+
throw new Error('Open this session inside a git repository before using `/dev-loops inspect`.');
|
|
56
45
|
}
|
|
57
46
|
const repoRoot = `${result.stdout ?? ''}`.trim();
|
|
58
47
|
if (!repoRoot) {
|
|
@@ -62,7 +51,7 @@ async function getRepoRoot(pi: ExtensionAPI): Promise<string> {
|
|
|
62
51
|
}
|
|
63
52
|
|
|
64
53
|
export function createExtensionCoreRuntime(
|
|
65
|
-
|
|
54
|
+
adapter: ExtensionHarnessAdapter,
|
|
66
55
|
{
|
|
67
56
|
uiLifecycle = createInspectRunViewerLifecycleManager(),
|
|
68
57
|
getRepoRoot: getRepoRootOverride,
|
|
@@ -73,13 +62,13 @@ export function createExtensionCoreRuntime(
|
|
|
73
62
|
) {
|
|
74
63
|
return {
|
|
75
64
|
surface: 'extension' as const,
|
|
76
|
-
commandExists: (command: string) => commandExists(
|
|
77
|
-
ghAuthOk: () => ghAuthOk(
|
|
78
|
-
insideGitRepo: () => insideGitRepo(
|
|
79
|
-
getRepoRoot: () => (getRepoRootOverride ? getRepoRootOverride() : getRepoRoot(
|
|
65
|
+
commandExists: (command: string) => commandExists(adapter, command),
|
|
66
|
+
ghAuthOk: () => ghAuthOk(adapter),
|
|
67
|
+
insideGitRepo: () => insideGitRepo(adapter),
|
|
68
|
+
getRepoRoot: () => (getRepoRootOverride ? getRepoRootOverride() : getRepoRoot(adapter)),
|
|
80
69
|
uiLifecycle,
|
|
81
70
|
async getSubagentAvailability() {
|
|
82
|
-
const ok = await commandExists(
|
|
71
|
+
const ok = await commandExists(adapter, 'subagent');
|
|
83
72
|
return {
|
|
84
73
|
ok,
|
|
85
74
|
availableDetail: '`subagent` command is available.',
|
|
@@ -89,6 +78,6 @@ export function createExtensionCoreRuntime(
|
|
|
89
78
|
};
|
|
90
79
|
}
|
|
91
80
|
|
|
92
|
-
export async function collectDevLoopChecks(
|
|
93
|
-
return collectSharedDevLoopChecks(createExtensionCoreRuntime(
|
|
81
|
+
export async function collectDevLoopChecks(adapter: ExtensionHarnessAdapter): Promise<DevLoopCheck[]> {
|
|
82
|
+
return collectSharedDevLoopChecks(createExtensionCoreRuntime(adapter));
|
|
94
83
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neutral, harness-agnostic types for the extension surface.
|
|
3
|
+
*
|
|
4
|
+
* These mirror the JSDoc typedefs in
|
|
5
|
+
* `packages/core/src/harness/extension-adapter.mjs` so the `.ts` extension modules
|
|
6
|
+
* can share a single import-free type vocabulary. No `@earendil-works/pi-*` import
|
|
7
|
+
* lives here — that boundary is owned solely by `./pi-extension-adapter.ts`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type HarnessExecResult = {
|
|
11
|
+
code?: number;
|
|
12
|
+
stdout?: string;
|
|
13
|
+
stderr?: string;
|
|
14
|
+
killed?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type HarnessExecOptions = {
|
|
18
|
+
cwd?: string;
|
|
19
|
+
timeout?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type HarnessUi = {
|
|
23
|
+
notify(message: string, level?: 'info' | 'warning' | 'error'): void;
|
|
24
|
+
setWidget(key: string, lines: string[] | undefined, options?: Record<string, unknown>): void;
|
|
25
|
+
setStatus(key: string, text: string | undefined): void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type HarnessContext = {
|
|
29
|
+
cwd: string;
|
|
30
|
+
hasUI: boolean;
|
|
31
|
+
ui: HarnessUi;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type HarnessLifecycleEvent = 'session_start' | 'tool_result' | 'user_bash' | 'agent_end';
|
|
35
|
+
|
|
36
|
+
export type HarnessCommandConfig = {
|
|
37
|
+
description: string;
|
|
38
|
+
handler: (args: string | string[], ctx: HarnessContext) => unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type HarnessLifecycleHandler = (event: unknown, ctx: HarnessContext) => unknown;
|
|
42
|
+
|
|
43
|
+
export type ExtensionHarnessAdapter = {
|
|
44
|
+
exec(command: string, options?: HarnessExecOptions): Promise<HarnessExecResult>;
|
|
45
|
+
on(event: HarnessLifecycleEvent, handler: HarnessLifecycleHandler): void;
|
|
46
|
+
registerCommand(name: string, config: HarnessCommandConfig): void;
|
|
47
|
+
};
|
package/extension/index.ts
CHANGED
|
@@ -2,11 +2,11 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
6
5
|
|
|
7
6
|
import { executeDevLoopsCommand } from '../lib/dev-loops-core.mjs';
|
|
8
7
|
import { createExtensionCoreRuntime } from './checks.ts';
|
|
9
8
|
import { createPostMergeUpdateHook } from './post-merge-update.ts';
|
|
9
|
+
import { createPiExtensionAdapter, type ExtensionAPI } from './pi-extension-adapter.ts';
|
|
10
10
|
import {
|
|
11
11
|
buildHelpLines,
|
|
12
12
|
buildInspectLines,
|
|
@@ -45,9 +45,11 @@ export function syncPackagedAgents({
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export default function (pi: ExtensionAPI, runtimeOverrides: ExtensionRuntimeOverrides = {}) {
|
|
48
|
-
|
|
48
|
+
// Wrap the Pi harness at the entry boundary; everything below talks to the neutral seam.
|
|
49
|
+
const adapter = createPiExtensionAdapter(pi);
|
|
50
|
+
const postMergeUpdateHook = runtimeOverrides.postMergeUpdateHook ?? createPostMergeUpdateHook({ exec: adapter.exec });
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
adapter.on('session_start', async (_event, ctx) => {
|
|
51
53
|
postMergeUpdateHook.onSessionStart();
|
|
52
54
|
try {
|
|
53
55
|
syncPackagedAgents();
|
|
@@ -57,25 +59,25 @@ export default function (pi: ExtensionAPI, runtimeOverrides: ExtensionRuntimeOve
|
|
|
57
59
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
58
60
|
});
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
adapter.on('tool_result', async (event, ctx) => {
|
|
61
63
|
await postMergeUpdateHook.onToolResult(event, ctx);
|
|
62
64
|
});
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
adapter.on('user_bash', async (event, ctx) => {
|
|
65
67
|
return postMergeUpdateHook.onUserBash(event, ctx);
|
|
66
68
|
});
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
adapter.on('agent_end', async (event, ctx) => {
|
|
69
71
|
await postMergeUpdateHook.onAgentEnd(event, ctx);
|
|
70
72
|
});
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
adapter.registerCommand('dev-loops', {
|
|
73
75
|
description: 'Manage dev-loops readiness and inspect-run local UI lifecycle: /dev-loops [help|status|doctor|gates|hide|inspect ...]',
|
|
74
76
|
handler: async (args, ctx) => {
|
|
75
77
|
const result = await executeDevLoopsCommand({
|
|
76
78
|
input: args,
|
|
77
79
|
surface: 'extension',
|
|
78
|
-
runtime: createExtensionCoreRuntime(
|
|
80
|
+
runtime: createExtensionCoreRuntime(adapter, runtimeOverrides),
|
|
79
81
|
});
|
|
80
82
|
|
|
81
83
|
switch (result.kind) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
|
|
3
|
+
import { createExtensionHarnessAdapter } from '@dev-loops/core/harness';
|
|
4
|
+
|
|
5
|
+
// Re-exported so the rest of the extension can name the Pi entry type without importing
|
|
6
|
+
// `@earendil-works/pi-*` directly — this module owns that import boundary.
|
|
7
|
+
export type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ExtensionHarnessAdapter,
|
|
11
|
+
HarnessContext,
|
|
12
|
+
HarnessCommandConfig,
|
|
13
|
+
HarnessExecOptions,
|
|
14
|
+
HarnessLifecycleEvent,
|
|
15
|
+
HarnessLifecycleHandler,
|
|
16
|
+
} from './harness-types.ts';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The single module allowed to import `@earendil-works/pi-*`.
|
|
20
|
+
*
|
|
21
|
+
* Wraps a Pi `ExtensionAPI` into the neutral `ExtensionHarnessAdapter` so the rest of
|
|
22
|
+
* the extension talks only to the neutral seam. Pi's per-invocation `ExtensionContext`
|
|
23
|
+
* is converted to a neutral `HarnessContext` at call time, so UI/cwd flow through
|
|
24
|
+
* unchanged — preserving today's behavior exactly.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export function toHarnessContext(ctx: Partial<ExtensionContext> | undefined): HarnessContext {
|
|
28
|
+
const ui = ctx?.ui;
|
|
29
|
+
return {
|
|
30
|
+
cwd: (ctx?.cwd as string) ?? process.cwd(),
|
|
31
|
+
hasUI: Boolean(ctx?.hasUI),
|
|
32
|
+
ui: {
|
|
33
|
+
notify: (message, level = 'info') => ui?.notify?.(message, level),
|
|
34
|
+
setWidget: (key, lines, options) => ui?.setWidget?.(key, lines as never, options as never),
|
|
35
|
+
setStatus: (key, text) => ui?.setStatus?.(key, text as never),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createPiExtensionAdapter(pi: ExtensionAPI): ExtensionHarnessAdapter {
|
|
41
|
+
// Route through the shared factory so the Pi adapter gets the same validation + freeze
|
|
42
|
+
// guarantee as the Claude adapter (symmetric construction across harnesses).
|
|
43
|
+
return createExtensionHarnessAdapter({
|
|
44
|
+
async exec(command: string, options: HarnessExecOptions = {}) {
|
|
45
|
+
return pi.exec('bash', ['-lc', command], {
|
|
46
|
+
cwd: options.cwd,
|
|
47
|
+
timeout: options.timeout,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
on(event: HarnessLifecycleEvent, handler: HarnessLifecycleHandler) {
|
|
52
|
+
pi.on(event as never, (async (piEvent: unknown, piCtx: ExtensionContext) =>
|
|
53
|
+
handler(piEvent, toHarnessContext(piCtx))) as never);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
registerCommand(name: string, config: HarnessCommandConfig) {
|
|
57
|
+
pi.registerCommand(name, {
|
|
58
|
+
description: config.description,
|
|
59
|
+
handler: (async (args: string | string[], piCtx: ExtensionContext) =>
|
|
60
|
+
config.handler(args, toHarnessContext(piCtx))) as never,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
}) as ExtensionHarnessAdapter;
|
|
64
|
+
}
|