@punks/cli 1.0.6 → 1.0.7
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/dist/data/catalog/hooks.ts +0 -8
- package/dist/data/catalog/packs.ts +2 -2
- package/dist/data/hooks.test.ts +0 -29
- package/dist/data/scripts/sync-subagents.mjs +4 -55
- package/dist/index.js +5 -9
- package/dist/skills/agnostic/planning/implement-spec/SKILL.md +5 -7
- package/dist/skills/agnostic/planning/implement-spec/references/parallel-orchestration.md +0 -1
- package/dist/skills/agnostic/planning/implement-spec/references/parallel-worker-brief.md +0 -3
- package/dist/skills/agnostic/planning/implement-spec/references/parallel.md +5 -7
- package/docs/README.md +7 -1
- package/docs/harness-intelligence-grill-log.md +39 -0
- package/docs/harness-intelligence-grill-status.md +25 -0
- package/docs/runbooks/dp-cli-scaffolding.md +1 -1
- package/package.json +1 -1
- package/dist/data/hooks/format-edited-file.py +0 -157
- package/dist/data/hooks/require-tests-for-pr.mjs +0 -206
- package/dist/skills/agnostic/planning/implement-spec/references/parallel-reasoning.md +0 -19
|
@@ -15,12 +15,4 @@ export const hookCatalog = [
|
|
|
15
15
|
"Shared hook that auto-formats edited JS/TS/JSON files and lint-checks product files after tool use.",
|
|
16
16
|
sourcePath: "hooks/format-edited-file.mjs",
|
|
17
17
|
},
|
|
18
|
-
{
|
|
19
|
-
id: "require-tests-for-pr",
|
|
20
|
-
harness: "shared",
|
|
21
|
-
outputPath: ".agents/hooks/require-tests-for-pr.mjs",
|
|
22
|
-
description:
|
|
23
|
-
"Shared hook that blocks PR creation while tests are failing.",
|
|
24
|
-
sourcePath: "hooks/require-tests-for-pr.mjs",
|
|
25
|
-
},
|
|
26
18
|
] as const satisfies ReadonlyArray<HookDefinition>;
|
|
@@ -2,7 +2,7 @@ import type { PackCategory, PackId } from "../../core/models";
|
|
|
2
2
|
import type { LintAssetId } from "./lint";
|
|
3
3
|
import type { SkillId } from "./skills";
|
|
4
4
|
|
|
5
|
-
export type PackHookId = "format-edited-file"
|
|
5
|
+
export type PackHookId = "format-edited-file";
|
|
6
6
|
export type PackPromptSurfaceId =
|
|
7
7
|
| "shared-agents"
|
|
8
8
|
| "root-prompt-spec"
|
|
@@ -42,5 +42,5 @@ export const packCatalog = [
|
|
|
42
42
|
{ id: "trpc", category: "detected", triggerPackages: ["@trpc/"], description: "tRPC guidance pack.", skills: [], lintAssets: [], hooks: [], promptSurfaces: ["shared-agents", "root-prompt-spec", "workspace-prompt-spec"], promptDetails: ["shared contract-first baseline", "workspace client/server boundaries"] },
|
|
43
43
|
{ id: "turborepo", category: "detected", triggerPackages: ["turbo"], description: "Turborepo guidance pack.", skills: ["turborepo"], lintAssets: [], hooks: [], promptSurfaces: ["shared-agents", "root-prompt-spec", "workspace-prompt-spec"], promptDetails: ["shared monorepo baseline", "workspace task boundary rules"] },
|
|
44
44
|
{ id: "python", category: "language", triggerPackages: [], description: "Shared Python language pack for async, style, structure, testing, and design guidance.", skills: ["async-python-patterns", "python-code-style", "python-design-patterns", "python-project-structure", "python-testing-patterns"], lintAssets: [], hooks: [], promptSurfaces: [], promptDetails: [] },
|
|
45
|
-
{ id: "typescript", category: "language", triggerPackages: ["typescript"], description: "Shared JavaScript / TypeScript language pack for hook and language-level scaffold assets.", skills: ["quality-types"], lintAssets: [], hooks: ["format-edited-file"
|
|
45
|
+
{ id: "typescript", category: "language", triggerPackages: ["typescript"], description: "Shared JavaScript / TypeScript language pack for hook and language-level scaffold assets.", skills: ["quality-types"], lintAssets: [], hooks: ["format-edited-file"], promptSurfaces: [], promptDetails: [] },
|
|
46
46
|
] as const satisfies ReadonlyArray<PackCatalogEntry>;
|
package/dist/data/hooks.test.ts
CHANGED
|
@@ -31,35 +31,6 @@ const tempRepo = (files: Record<string, string>) => {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
describe("scaffolded hooks", () => {
|
|
34
|
-
it("runs the PR test gate with the repo package manager", async () => {
|
|
35
|
-
const { testHooks } = await loadHookModule("require-tests-for-pr.mjs");
|
|
36
|
-
|
|
37
|
-
expect(
|
|
38
|
-
testHooks.testCommand?.(
|
|
39
|
-
tempRepo({
|
|
40
|
-
"package.json": JSON.stringify({ packageManager: "bun@1.3.5" }),
|
|
41
|
-
}),
|
|
42
|
-
),
|
|
43
|
-
).toEqual(["bun", "run", "test"]);
|
|
44
|
-
|
|
45
|
-
expect(
|
|
46
|
-
testHooks.testCommand?.(
|
|
47
|
-
tempRepo({
|
|
48
|
-
"package.json": JSON.stringify({ packageManager: "pnpm@10.0.0" }),
|
|
49
|
-
}),
|
|
50
|
-
),
|
|
51
|
-
).toEqual(["pnpm", "--silent", "test"]);
|
|
52
|
-
|
|
53
|
-
expect(
|
|
54
|
-
testHooks.testCommand?.(
|
|
55
|
-
tempRepo({
|
|
56
|
-
"package.json": "{}",
|
|
57
|
-
"package-lock.json": "",
|
|
58
|
-
}),
|
|
59
|
-
),
|
|
60
|
-
).toEqual(["npm", "--silent", "test"]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
34
|
it("executes formatter tools with the repo package manager", async () => {
|
|
64
35
|
const { testHooks } = await loadHookModule("format-edited-file.mjs");
|
|
65
36
|
|
|
@@ -52,7 +52,7 @@ const cursorManagedComment =
|
|
|
52
52
|
const opencodeManagedComment =
|
|
53
53
|
"<!-- Generated by scripts/sync-subagents.mjs. Edit .agents/subagents/manifest.mjs instead. -->";
|
|
54
54
|
|
|
55
|
-
function buildCodexAgentConfig({ hasFormatHook
|
|
55
|
+
function buildCodexAgentConfig({ hasFormatHook }) {
|
|
56
56
|
const lines = [
|
|
57
57
|
"[agents]",
|
|
58
58
|
"max_threads = 6",
|
|
@@ -74,16 +74,6 @@ function buildCodexAgentConfig({ hasFormatHook, hasTestHook }) {
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
if (hasTestHook) {
|
|
78
|
-
lines.push(
|
|
79
|
-
"PreToolUse = [",
|
|
80
|
-
' { matcher = "Bash|.*[Pp]ull.?[Rr]equest.*|.*[Pp][Rr].?[Cc]reate.*|.*create[_-]?pull[_-]?request.*", hooks = [',
|
|
81
|
-
' { type = "command", command = "node \\"$(git rev-parse --show-toplevel)/.codex/hooks/require-tests-for-pr.mjs\\" codex", statusMessage = "Checking tests before PR creation", timeout = 600 },',
|
|
82
|
-
" ] },",
|
|
83
|
-
"]",
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
77
|
if (hasFormatHook) {
|
|
88
78
|
lines.push(
|
|
89
79
|
"PostToolUse = [",
|
|
@@ -98,35 +88,9 @@ function buildCodexAgentConfig({ hasFormatHook, hasTestHook }) {
|
|
|
98
88
|
return lines.join("\n");
|
|
99
89
|
}
|
|
100
90
|
|
|
101
|
-
function buildClaudeSettingsConfig({ hasFormatHook
|
|
91
|
+
function buildClaudeSettingsConfig({ hasFormatHook }) {
|
|
102
92
|
return {
|
|
103
93
|
hooks: {
|
|
104
|
-
...(hasTestHook
|
|
105
|
-
? {
|
|
106
|
-
PreToolUse: [
|
|
107
|
-
{
|
|
108
|
-
matcher: "mcp__github__create_pull_request",
|
|
109
|
-
hooks: [
|
|
110
|
-
{
|
|
111
|
-
type: "command",
|
|
112
|
-
command: 'node "$CLAUDE_PROJECT_DIR"/.claude/hooks/require-tests-for-pr.mjs claude',
|
|
113
|
-
timeout: 600,
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
matcher: "Bash",
|
|
119
|
-
hooks: [
|
|
120
|
-
{
|
|
121
|
-
type: "command",
|
|
122
|
-
command: 'node "$CLAUDE_PROJECT_DIR"/.claude/hooks/require-tests-for-pr.mjs claude',
|
|
123
|
-
timeout: 600,
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
}
|
|
129
|
-
: {}),
|
|
130
94
|
...(hasFormatHook
|
|
131
95
|
? {
|
|
132
96
|
PostToolUse: [
|
|
@@ -147,24 +111,10 @@ function buildClaudeSettingsConfig({ hasFormatHook, hasTestHook }) {
|
|
|
147
111
|
};
|
|
148
112
|
}
|
|
149
113
|
|
|
150
|
-
function buildCursorHooksConfig({ hasFormatHook
|
|
114
|
+
function buildCursorHooksConfig({ hasFormatHook }) {
|
|
151
115
|
return {
|
|
152
116
|
version: 1,
|
|
153
117
|
hooks: {
|
|
154
|
-
...(hasTestHook
|
|
155
|
-
? {
|
|
156
|
-
beforeMCPExecution: [
|
|
157
|
-
{
|
|
158
|
-
command: "node .cursor/hooks/require-tests-for-pr.mjs cursor",
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
beforeShellExecution: [
|
|
162
|
-
{
|
|
163
|
-
command: "node .cursor/hooks/require-tests-for-pr.mjs cursor",
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
}
|
|
167
|
-
: {}),
|
|
168
118
|
...(hasFormatHook
|
|
169
119
|
? {
|
|
170
120
|
afterFileEdit: [
|
|
@@ -391,7 +341,7 @@ async function syncHookSurface() {
|
|
|
391
341
|
await mkdir(cursorHooksDir, { recursive: true });
|
|
392
342
|
await mkdir(opencodePluginsDir, { recursive: true });
|
|
393
343
|
|
|
394
|
-
for (const hookName of ["format-edited-file.mjs"
|
|
344
|
+
for (const hookName of ["format-edited-file.mjs"]) {
|
|
395
345
|
const sourcePath = path.join(sharedHooksPath, hookName);
|
|
396
346
|
const targetPaths = [
|
|
397
347
|
path.join(codexHooksDir, hookName),
|
|
@@ -445,7 +395,6 @@ async function main() {
|
|
|
445
395
|
await mkdir(path.dirname(cursorHooksJsonPath), { recursive: true });
|
|
446
396
|
const availableHooks = {
|
|
447
397
|
hasFormatHook: await pathExists(path.join(sharedHooksPath, "format-edited-file.mjs")),
|
|
448
|
-
hasTestHook: await pathExists(path.join(sharedHooksPath, "require-tests-for-pr.mjs")),
|
|
449
398
|
};
|
|
450
399
|
await writeFile(codexConfigPath, buildCodexAgentConfig(availableHooks), "utf8");
|
|
451
400
|
await rm(path.join(codexDir, "hooks.json"), { force: true });
|
package/dist/index.js
CHANGED
|
@@ -41910,13 +41910,6 @@ var hookCatalog = [
|
|
|
41910
41910
|
outputPath: ".agents/hooks/format-edited-file.mjs",
|
|
41911
41911
|
description: "Shared hook that auto-formats edited JS/TS/JSON files and lint-checks product files after tool use.",
|
|
41912
41912
|
sourcePath: "hooks/format-edited-file.mjs"
|
|
41913
|
-
},
|
|
41914
|
-
{
|
|
41915
|
-
id: "require-tests-for-pr",
|
|
41916
|
-
harness: "shared",
|
|
41917
|
-
outputPath: ".agents/hooks/require-tests-for-pr.mjs",
|
|
41918
|
-
description: "Shared hook that blocks PR creation while tests are failing.",
|
|
41919
|
-
sourcePath: "hooks/require-tests-for-pr.mjs"
|
|
41920
41913
|
}
|
|
41921
41914
|
];
|
|
41922
41915
|
|
|
@@ -42215,7 +42208,7 @@ var packCatalog = [
|
|
|
42215
42208
|
{ id: "trpc", category: "detected", triggerPackages: ["@trpc/"], description: "tRPC guidance pack.", skills: [], lintAssets: [], hooks: [], promptSurfaces: ["shared-agents", "root-prompt-spec", "workspace-prompt-spec"], promptDetails: ["shared contract-first baseline", "workspace client/server boundaries"] },
|
|
42216
42209
|
{ id: "turborepo", category: "detected", triggerPackages: ["turbo"], description: "Turborepo guidance pack.", skills: ["turborepo"], lintAssets: [], hooks: [], promptSurfaces: ["shared-agents", "root-prompt-spec", "workspace-prompt-spec"], promptDetails: ["shared monorepo baseline", "workspace task boundary rules"] },
|
|
42217
42210
|
{ id: "python", category: "language", triggerPackages: [], description: "Shared Python language pack for async, style, structure, testing, and design guidance.", skills: ["async-python-patterns", "python-code-style", "python-design-patterns", "python-project-structure", "python-testing-patterns"], lintAssets: [], hooks: [], promptSurfaces: [], promptDetails: [] },
|
|
42218
|
-
{ id: "typescript", category: "language", triggerPackages: ["typescript"], description: "Shared JavaScript / TypeScript language pack for hook and language-level scaffold assets.", skills: ["quality-types"], lintAssets: [], hooks: ["format-edited-file"
|
|
42211
|
+
{ id: "typescript", category: "language", triggerPackages: ["typescript"], description: "Shared JavaScript / TypeScript language pack for hook and language-level scaffold assets.", skills: ["quality-types"], lintAssets: [], hooks: ["format-edited-file"], promptSurfaces: [], promptDetails: [] }
|
|
42219
42212
|
];
|
|
42220
42213
|
|
|
42221
42214
|
// src/data/catalog/skills.ts
|
|
@@ -42296,7 +42289,7 @@ var toolCatalog = [
|
|
|
42296
42289
|
|
|
42297
42290
|
// package.json
|
|
42298
42291
|
var name = "@punks/cli";
|
|
42299
|
-
var version = "1.0.
|
|
42292
|
+
var version = "1.0.7";
|
|
42300
42293
|
// src/baseline/bundled.ts
|
|
42301
42294
|
var bundledBaseline = {
|
|
42302
42295
|
summary: {
|
|
@@ -46694,6 +46687,9 @@ var runUpdate = ({
|
|
|
46694
46687
|
relativePath: change.path
|
|
46695
46688
|
});
|
|
46696
46689
|
}
|
|
46690
|
+
for (const staleFile of staleFiles) {
|
|
46691
|
+
rmSync3(path12.join(rootDirectory, staleFile), { recursive: true, force: true });
|
|
46692
|
+
}
|
|
46697
46693
|
}
|
|
46698
46694
|
if (check2 && needsAttention) {
|
|
46699
46695
|
process.exitCode = 1;
|
|
@@ -41,12 +41,11 @@ That means `implement-spec` itself owns all of the following in parallel mode:
|
|
|
41
41
|
3. Choose the execution mode explicitly:
|
|
42
42
|
- Read `references/sequential.md` for one-thread execution.
|
|
43
43
|
- Read `references/parallel.md` for wave-based worker execution.
|
|
44
|
-
4.
|
|
45
|
-
5.
|
|
46
|
-
6.
|
|
47
|
-
7.
|
|
48
|
-
8.
|
|
49
|
-
9. Finish with the shared acceptance audit and spec finalization contract.
|
|
44
|
+
4. Record the chosen mode under **Execution mode** in `IMPLEMENTATION-NOTES.md` before coding.
|
|
45
|
+
5. Execute only the chosen mode. Do not mix modes inside one run.
|
|
46
|
+
6. After each completed task or wave, update `PLAN.md`, `IMPLEMENTATION-NOTES.md`, and spec-linked tech debt before advancing.
|
|
47
|
+
7. If backlog sync is in scope, keep epic/story bodies product-facing and use native metadata or comments instead of execution handoff rewrites.
|
|
48
|
+
8. Finish with the shared acceptance audit and spec finalization contract.
|
|
50
49
|
|
|
51
50
|
## Mode selection
|
|
52
51
|
|
|
@@ -71,4 +70,3 @@ If the user already chose a mode, honor it. If not, make the smallest safe choic
|
|
|
71
70
|
- Parallel execution specifics: see [references/parallel.md](references/parallel.md)
|
|
72
71
|
- Parallel plan parsing and wave construction: see [references/parallel-orchestration.md](references/parallel-orchestration.md)
|
|
73
72
|
- Parallel worker brief contract: see [references/parallel-worker-brief.md](references/parallel-worker-brief.md)
|
|
74
|
-
- Codex parallel worker reasoning policy: see [references/parallel-reasoning.md](references/parallel-reasoning.md)
|
|
@@ -56,7 +56,6 @@ Launch all unblocked tasks in parallel.
|
|
|
56
56
|
For each unblocked task:
|
|
57
57
|
|
|
58
58
|
- choose the worker template from `.agents/subagents/manifest.mjs`
|
|
59
|
-
- in Codex, apply the worker `reasoning_effort` policy from [parallel-reasoning.md](parallel-reasoning.md)
|
|
60
59
|
- use the worker-brief contract from [parallel-worker-brief.md](parallel-worker-brief.md)
|
|
61
60
|
- keep the task scope narrow
|
|
62
61
|
- ensure the worker owns only the assigned task and its required validation
|
|
@@ -34,8 +34,6 @@ Each worker brief should require:
|
|
|
34
34
|
9. running the exact task validation evidence before returning, plus extra plan validation when feasible
|
|
35
35
|
10. updating the plan entry with status, log, touched files, and gotchas before handoff closes
|
|
36
36
|
|
|
37
|
-
When spawning Codex workers, apply [parallel-reasoning.md](parallel-reasoning.md): `xhigh -> high`, `high -> medium`, `medium -> low`, `low -> low`.
|
|
38
|
-
|
|
39
37
|
## Worker output contract
|
|
40
38
|
|
|
41
39
|
Require the worker to return:
|
|
@@ -65,4 +63,3 @@ The parent `implement-spec` run owns:
|
|
|
65
63
|
- review of worker outputs
|
|
66
64
|
- retry and escalation decisions
|
|
67
65
|
- deciding when the wave is complete
|
|
68
|
-
- the final acceptance audit
|
|
@@ -31,18 +31,16 @@ Do not treat worker spawning as the whole job. The orchestration loop is part of
|
|
|
31
31
|
1. Load the shared lifecycle from `lifecycle.md`.
|
|
32
32
|
2. Record `parallel` under **Execution mode** in `IMPLEMENTATION-NOTES.md`.
|
|
33
33
|
3. Read `.agents/subagents/manifest.mjs` before the first spawn and choose explicit worker templates per task.
|
|
34
|
-
4.
|
|
35
|
-
5.
|
|
36
|
-
6.
|
|
37
|
-
7.
|
|
38
|
-
8.
|
|
39
|
-
9. Update the plan, notes, and tech debt after every wave.
|
|
34
|
+
4. Read [parallel-orchestration.md](parallel-orchestration.md) and parse `PLAN.md` into a task graph.
|
|
35
|
+
5. Build the current wave from the unblocked tasks only.
|
|
36
|
+
6. Read [parallel-worker-brief.md](parallel-worker-brief.md) and use that contract when spawning workers.
|
|
37
|
+
7. Validate each wave before moving on. Fix failures before the next wave.
|
|
38
|
+
8. Update the plan, notes, and tech debt after every wave.
|
|
40
39
|
|
|
41
40
|
## Required evidence
|
|
42
41
|
|
|
43
42
|
- plan-derived wave selection
|
|
44
43
|
- explicit worker briefs per task
|
|
45
|
-
- explicit Codex worker `reasoning_effort` when running in Codex
|
|
46
44
|
- post-wave review of worker outputs
|
|
47
45
|
- acceptance-criteria coverage plus RED -> GREEN evidence, or explicit non-testable verification
|
|
48
46
|
- task completion only after validation and plan/log updates
|
package/docs/README.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
- [Requirements](./reference/dp-requirements.md)
|
|
4
4
|
- [Scaffolding runbook](./runbooks/dp-cli-scaffolding.md)
|
|
5
5
|
|
|
6
|
+
Domain knowledge:
|
|
7
|
+
|
|
8
|
+
- [Wiki index](../wiki/index.md)
|
|
9
|
+
|
|
10
|
+
The wiki is the durable domain layer for specs, raw inputs, flows, and concepts. Keep operator runbooks and implementation reference in `docs/`; keep stable product/domain knowledge in `wiki/`.
|
|
11
|
+
|
|
6
12
|
Implementation notes:
|
|
7
13
|
|
|
8
14
|
- canonical bundled scaffold metadata and shared assets live in `src/data/`
|
|
@@ -16,7 +22,7 @@ Implementation notes:
|
|
|
16
22
|
- shared neutral hook and sync assets live in `src/data/hooks/` and `src/data/scripts/`; hook commands infer the target repo package manager from `packageManager` and lockfiles
|
|
17
23
|
- scaffolded required tools always include `portless` and `skills` so generated guidance can standardize local dev origins and keep skill entrypoints up to date
|
|
18
24
|
- `punks scaffold setup` checks the base required tools (`portless`, `skills`) before repo detection and checks selected-pack tools after pack confirmation.
|
|
19
|
-
- Oxlint specs/starter config are scaffolded only when scanned manifests already declare `oxlint`; the auto format/lint hook is scaffolded only when manifests declare `oxfmt`. Other lint/format stacks are intentionally left untouched for now.
|
|
25
|
+
- Oxlint specs/starter config are scaffolded only when scanned manifests already declare `oxlint`; the auto format/lint hook is scaffolded only when manifests declare `oxfmt`. PR creation is not gated by a scaffolded test-suite hook. Other lint/format stacks are intentionally left untouched for now.
|
|
20
26
|
- the default debug pack scaffolds the local `debug-agent` skill and installs/verifies the `debug-agent` CLI without running its agent-install wizard
|
|
21
27
|
- scaffolded repos keep project-local skills in `.agents/skills/`; only `.claude/skills` is a compatibility symlink mirror
|
|
22
28
|
- React scaffold surfaces include `async-react-patterns` alongside the existing React composition, structure, and Vercel guidance so agents avoid outdated manual async state patterns.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Harness Intelligence Grill Log
|
|
2
|
+
|
|
3
|
+
This log records locked requirements decisions for rethinking this repo as the Harness Intelligence project.
|
|
4
|
+
|
|
5
|
+
## Initial Situation
|
|
6
|
+
|
|
7
|
+
The repo currently provides real value as the `punks`/`dp` scaffolding CLI plus canonical scaffold baseline data. It has battle-tested workflow assets, repo-aware setup behavior, baseline publishing, and a local wiki-shaped knowledge tree.
|
|
8
|
+
|
|
9
|
+
The repo does not yet provide the full product/distribution surface needed for wider colleague adoption or public company communication:
|
|
10
|
+
|
|
11
|
+
- no Turborepo application layout
|
|
12
|
+
- no public Fumadocs wiki/docs app
|
|
13
|
+
- no public landing page
|
|
14
|
+
- no Effect backend for scaffold distribution, validation, or future orchestration
|
|
15
|
+
- no settled CLI-vs-backend boundary
|
|
16
|
+
- no durable explanation of harness theory, criteria, trust model, and sales/marketing narrative
|
|
17
|
+
|
|
18
|
+
## Branches
|
|
19
|
+
|
|
20
|
+
### Branch A: Product Boundary
|
|
21
|
+
|
|
22
|
+
Purpose: define what "Harness Intelligence" is as a product/system, and what this repo must own.
|
|
23
|
+
|
|
24
|
+
### Branch B: Distribution Architecture
|
|
25
|
+
|
|
26
|
+
Purpose: define Turborepo shape, app/package boundaries, backend role, CLI role, baseline/data flow, and public surfaces.
|
|
27
|
+
|
|
28
|
+
### Branch C: Wiki and Theory
|
|
29
|
+
|
|
30
|
+
Purpose: define the docs knowledge model, Fumadocs information architecture, and the harness engineering criteria currently living in the user's head.
|
|
31
|
+
|
|
32
|
+
### Branch D: Trust and Adoption
|
|
33
|
+
|
|
34
|
+
Purpose: define what colleagues need to trust the AI harness setup, how validation works, and what proof material must exist.
|
|
35
|
+
|
|
36
|
+
### Branch E: Company Communication
|
|
37
|
+
|
|
38
|
+
Purpose: define landing-page and sales narrative for how Devpunks engineers are AI-superpowered without becoming vague marketing copy.
|
|
39
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Harness Intelligence Grill Status
|
|
2
|
+
|
|
3
|
+
Current grill artifacts:
|
|
4
|
+
|
|
5
|
+
- Log: `docs/harness-intelligence-grill-log.md`
|
|
6
|
+
- Status: `docs/harness-intelligence-grill-status.md`
|
|
7
|
+
|
|
8
|
+
## Branch Dashboard
|
|
9
|
+
|
|
10
|
+
| Branch | Completion | Locked direction | Still open |
|
|
11
|
+
| --- | ---: | --- | --- |
|
|
12
|
+
| Product Boundary | 10% | Repo is being reconsidered as the Harness Intelligence project, not only a CLI package. | Canonical product definition, audience split, repo ownership boundary. |
|
|
13
|
+
| Distribution Architecture | 10% | Target mentions Turborepo, public docs app, landing page, Effect backend, and CLI. | App/package boundaries, backend-vs-CLI responsibilities, data/source-of-truth flow. |
|
|
14
|
+
| Wiki and Theory | 10% | Existing wiki shape exists locally; missing public Fumadocs app and full theory/criteria content. | IA, criteria taxonomy, private vs public knowledge split, docs ownership. |
|
|
15
|
+
| Trust and Adoption | 10% | Colleague recognition/validation is a primary requirement. | Trust evidence, onboarding path, validation gates, adoption metrics. |
|
|
16
|
+
| Company Communication | 10% | Public explanation must support sales/marketing around AI-superpowered developers. | Landing page promise, proof points, tone, relationship to devpunks.com. |
|
|
17
|
+
|
|
18
|
+
## Parked Branches
|
|
19
|
+
|
|
20
|
+
- None yet.
|
|
21
|
+
|
|
22
|
+
## Current Question
|
|
23
|
+
|
|
24
|
+
Q1: Product Boundary.
|
|
25
|
+
|
|
@@ -95,7 +95,7 @@ Current scope:
|
|
|
95
95
|
- check the base required tools (`portless`, `skills`) at the start of setup before repo detection, then check selected-pack tools after pack confirmation
|
|
96
96
|
- include `debug-agent` through the default debug pack and install/verify the `debug-agent` CLI without running `debug-agent init`, because the CLI already scaffolds the project-local skill
|
|
97
97
|
- scaffold Oxlint specs/starter config only when scanned package manifests declare `oxlint`, and scaffold the `format-edited-file` Oxfmt/Oxlint hook only when manifests declare `oxfmt`; repos without those tools keep their existing lint/format setup untouched
|
|
98
|
-
- scaffolded hooks infer the target repo package manager from `packageManager` first, then lockfiles, so
|
|
98
|
+
- scaffolded hooks infer the target repo package manager from `packageManager` first, then lockfiles, so Oxfmt/Oxlint execution does not hardcode the CLI repo's package manager
|
|
99
99
|
- select language packs separately from framework packs; TypeScript is selected from a `typescript` package dependency or nested `.ts` / `.tsx` files, and Python is selected from nested `.py` files, while ignoring root config files plus vendor, virtualenv, scaffold, docs, examples, scripts, `opensrc`, cache, and build output
|
|
100
100
|
- seed Python subagent templates that combine the Python language skills into `python-app`, `python-async`, and `python-testing` specialists
|
|
101
101
|
- seed a read-only `code-review` subagent template that uses `simplify` for changed-code cleanup review and `improve-codebase-architecture` for grounded architecture-friction findings
|
package/package.json
CHANGED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import hashlib
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import subprocess
|
|
9
|
-
import sys
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
SUPPORTED_SUFFIXES = (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json")
|
|
13
|
-
IGNORED_PARTS = {".git", "node_modules", ".next", "dist"}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def read_input() -> dict[str, object]:
|
|
17
|
-
try:
|
|
18
|
-
return json.load(sys.stdin)
|
|
19
|
-
except json.JSONDecodeError:
|
|
20
|
-
return {}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def run_git(args: list[str], cwd: str) -> str:
|
|
24
|
-
result = subprocess.run(
|
|
25
|
-
["git", "-C", cwd, *args],
|
|
26
|
-
check=True,
|
|
27
|
-
capture_output=True,
|
|
28
|
-
text=True,
|
|
29
|
-
)
|
|
30
|
-
return result.stdout
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def repo_root(cwd: str) -> str:
|
|
34
|
-
try:
|
|
35
|
-
return run_git(["rev-parse", "--show-toplevel"], cwd).strip()
|
|
36
|
-
except subprocess.CalledProcessError:
|
|
37
|
-
return ""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def dirty_files(root: str) -> list[str]:
|
|
41
|
-
try:
|
|
42
|
-
output = run_git(["ls-files", "-m", "-o", "--exclude-standard"], root)
|
|
43
|
-
except subprocess.CalledProcessError:
|
|
44
|
-
return []
|
|
45
|
-
|
|
46
|
-
files: list[str] = []
|
|
47
|
-
for raw_path in output.splitlines():
|
|
48
|
-
path = raw_path.strip()
|
|
49
|
-
if not path:
|
|
50
|
-
continue
|
|
51
|
-
if not path.endswith(SUPPORTED_SUFFIXES):
|
|
52
|
-
continue
|
|
53
|
-
if any(part in IGNORED_PARTS for part in Path(path).parts):
|
|
54
|
-
continue
|
|
55
|
-
files.append(path)
|
|
56
|
-
|
|
57
|
-
return sorted(set(files))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def file_hash(path: Path) -> str:
|
|
61
|
-
digest = hashlib.sha256()
|
|
62
|
-
with path.open("rb") as handle:
|
|
63
|
-
for chunk in iter(lambda: handle.read(65536), b""):
|
|
64
|
-
digest.update(chunk)
|
|
65
|
-
return digest.hexdigest()
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def snapshot_path(root: str, session_id: str, tool_use_id: str) -> Path:
|
|
69
|
-
repo_name = Path(root).name or "repo"
|
|
70
|
-
safe_session_id = session_id.replace("/", "_")
|
|
71
|
-
safe_tool_use_id = tool_use_id.replace("/", "_")
|
|
72
|
-
state_dir = Path(os.environ.get("TMPDIR", "/tmp")) / "codex-hooks" / repo_name / safe_session_id
|
|
73
|
-
state_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
-
return state_dir / f"{safe_tool_use_id}.json"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def load_snapshot(path: Path) -> dict[str, str]:
|
|
78
|
-
if not path.exists():
|
|
79
|
-
return {}
|
|
80
|
-
try:
|
|
81
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
82
|
-
except (OSError, json.JSONDecodeError):
|
|
83
|
-
return {}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def fingerprints(root: str) -> dict[str, str]:
|
|
87
|
-
snapshot: dict[str, str] = {}
|
|
88
|
-
for relative_path in dirty_files(root):
|
|
89
|
-
absolute_path = Path(root) / relative_path
|
|
90
|
-
if absolute_path.exists():
|
|
91
|
-
snapshot[relative_path] = file_hash(absolute_path)
|
|
92
|
-
return snapshot
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def format_files(root: str, files: list[str]) -> list[str]:
|
|
96
|
-
failed: list[str] = []
|
|
97
|
-
for relative_path in files:
|
|
98
|
-
absolute_path = Path(root) / relative_path
|
|
99
|
-
result = subprocess.run(
|
|
100
|
-
["npx", "oxfmt", "--write", str(absolute_path)],
|
|
101
|
-
cwd=root,
|
|
102
|
-
stdout=subprocess.DEVNULL,
|
|
103
|
-
stderr=subprocess.DEVNULL,
|
|
104
|
-
check=False,
|
|
105
|
-
)
|
|
106
|
-
if result.returncode != 0:
|
|
107
|
-
failed.append(relative_path)
|
|
108
|
-
return failed
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def emit_failure(files: list[str]) -> None:
|
|
112
|
-
message = "Auto-format failed for " + ", ".join(files) + ". Review the file and format it manually if needed."
|
|
113
|
-
print(json.dumps({"systemMessage": message}))
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def main() -> int:
|
|
117
|
-
mode = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
118
|
-
payload = read_input()
|
|
119
|
-
|
|
120
|
-
cwd = str(payload.get("cwd") or os.getcwd())
|
|
121
|
-
session_id = str(payload.get("session_id") or "")
|
|
122
|
-
tool_use_id = str(payload.get("tool_use_id") or "")
|
|
123
|
-
root = repo_root(cwd)
|
|
124
|
-
|
|
125
|
-
if not root or not session_id or not tool_use_id:
|
|
126
|
-
return 0
|
|
127
|
-
|
|
128
|
-
state_file = snapshot_path(root, session_id, tool_use_id)
|
|
129
|
-
|
|
130
|
-
if mode == "pre":
|
|
131
|
-
state_file.write_text(json.dumps(fingerprints(root), sort_keys=True), encoding="utf-8")
|
|
132
|
-
return 0
|
|
133
|
-
|
|
134
|
-
if mode != "post":
|
|
135
|
-
return 0
|
|
136
|
-
|
|
137
|
-
previous = load_snapshot(state_file)
|
|
138
|
-
current = fingerprints(root)
|
|
139
|
-
|
|
140
|
-
try:
|
|
141
|
-
state_file.unlink()
|
|
142
|
-
except OSError:
|
|
143
|
-
pass
|
|
144
|
-
|
|
145
|
-
touched = [path for path, digest in current.items() if previous.get(path) != digest]
|
|
146
|
-
if not touched:
|
|
147
|
-
return 0
|
|
148
|
-
|
|
149
|
-
failed = format_files(root, touched)
|
|
150
|
-
if failed:
|
|
151
|
-
emit_failure(failed)
|
|
152
|
-
|
|
153
|
-
return 0
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if __name__ == "__main__":
|
|
157
|
-
raise SystemExit(main())
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
const denyMessage = "Tests are failing. Fix all test failures before creating a PR.";
|
|
9
|
-
const prCommandPattern = /\bgh\s+pr\s+create\b/i;
|
|
10
|
-
const prToolPattern = /(^|[._-])create[_-]?pull[_-]?request([._-]|$)/i;
|
|
11
|
-
const packageManagers = new Set(["bun", "pnpm", "npm", "yarn"]);
|
|
12
|
-
|
|
13
|
-
function readStdinJson() {
|
|
14
|
-
try {
|
|
15
|
-
return JSON.parse(readFileSync(0, "utf8"));
|
|
16
|
-
} catch {
|
|
17
|
-
return {};
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function runGit(args, cwd) {
|
|
22
|
-
const result = spawnSync("git", ["-C", cwd, ...args], {
|
|
23
|
-
encoding: "utf8",
|
|
24
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
return result.status === 0 ? result.stdout : null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function repoRoot(cwd) {
|
|
31
|
-
return runGit(["rev-parse", "--show-toplevel"], cwd)?.trim() ?? "";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function collectStrings(value, values = []) {
|
|
35
|
-
if (typeof value === "string") {
|
|
36
|
-
const trimmed = value.trim();
|
|
37
|
-
if (trimmed) {
|
|
38
|
-
values.push(trimmed);
|
|
39
|
-
}
|
|
40
|
-
return values;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (Array.isArray(value)) {
|
|
44
|
-
for (const entry of value) {
|
|
45
|
-
collectStrings(entry, values);
|
|
46
|
-
}
|
|
47
|
-
return values;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!value || typeof value !== "object") {
|
|
51
|
-
return values;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
for (const entry of Object.values(value)) {
|
|
55
|
-
collectStrings(entry, values);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return values;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function matchesPrAction(...values) {
|
|
62
|
-
const strings = [...new Set(values.flatMap((value) => collectStrings(value)))];
|
|
63
|
-
|
|
64
|
-
return strings.some((value) => prCommandPattern.test(value) || prToolPattern.test(value));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function readPackageJson(root) {
|
|
68
|
-
try {
|
|
69
|
-
return JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
70
|
-
} catch {
|
|
71
|
-
return {};
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function packageManagerFromPackageJson(root) {
|
|
76
|
-
const packageManager = readPackageJson(root)?.packageManager;
|
|
77
|
-
|
|
78
|
-
if (typeof packageManager !== "string") {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const name = packageManager.split("@")[0];
|
|
83
|
-
return packageManagers.has(name) ? name : null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function packageManagerFromLockfile(root) {
|
|
87
|
-
if (existsSync(path.join(root, "bun.lock")) || existsSync(path.join(root, "bun.lockb"))) {
|
|
88
|
-
return "bun";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
existsSync(path.join(root, "pnpm-lock.yaml")) ||
|
|
93
|
-
existsSync(path.join(root, "pnpm-workspace.yaml"))
|
|
94
|
-
) {
|
|
95
|
-
return "pnpm";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (existsSync(path.join(root, "yarn.lock"))) {
|
|
99
|
-
return "yarn";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (existsSync(path.join(root, "package-lock.json"))) {
|
|
103
|
-
return "npm";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function testCommand(root) {
|
|
110
|
-
switch (packageManagerFromPackageJson(root) ?? packageManagerFromLockfile(root) ?? "npm") {
|
|
111
|
-
case "bun":
|
|
112
|
-
return ["bun", "run", "test"];
|
|
113
|
-
case "pnpm":
|
|
114
|
-
return ["pnpm", "--silent", "test"];
|
|
115
|
-
case "yarn":
|
|
116
|
-
return ["yarn", "--silent", "test"];
|
|
117
|
-
case "npm":
|
|
118
|
-
default:
|
|
119
|
-
return ["npm", "--silent", "test"];
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function runTests(root) {
|
|
124
|
-
const command = testCommand(root);
|
|
125
|
-
const result = spawnSync(command[0], command.slice(1), {
|
|
126
|
-
cwd: root,
|
|
127
|
-
stdio: "ignore",
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
return result.status === 0;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function emitCodexOrClaudeBlock(message) {
|
|
134
|
-
process.stdout.write(
|
|
135
|
-
JSON.stringify({
|
|
136
|
-
hookSpecificOutput: {
|
|
137
|
-
hookEventName: "PreToolUse",
|
|
138
|
-
permissionDecision: "deny",
|
|
139
|
-
permissionDecisionReason: message,
|
|
140
|
-
},
|
|
141
|
-
systemMessage: message,
|
|
142
|
-
}),
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function emitCursorBlock(message) {
|
|
147
|
-
process.stdout.write(
|
|
148
|
-
JSON.stringify({
|
|
149
|
-
continue: false,
|
|
150
|
-
permission: "deny",
|
|
151
|
-
agent_message: message,
|
|
152
|
-
user_message: message,
|
|
153
|
-
}),
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function runCommandHook(style) {
|
|
158
|
-
const payload = readStdinJson();
|
|
159
|
-
const root = repoRoot(String(payload.cwd ?? process.cwd()));
|
|
160
|
-
|
|
161
|
-
if (!root || !matchesPrAction(payload) || runTests(root)) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (style === "cursor") {
|
|
166
|
-
emitCursorBlock(denyMessage);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
emitCodexOrClaudeBlock(denyMessage);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export const RequireTestsForPrPlugin = async ({ worktree }) => {
|
|
174
|
-
return {
|
|
175
|
-
"tool.execute.before": async (input, output) => {
|
|
176
|
-
if (!matchesPrAction(input?.tool, output?.args?.command)) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (runTests(worktree)) {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
throw new Error(denyMessage);
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
export const testHooks = {
|
|
190
|
-
testCommand,
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
function main() {
|
|
194
|
-
const mode = process.argv[2] ?? "";
|
|
195
|
-
|
|
196
|
-
if (mode === "claude" || mode === "codex" || mode === "cursor") {
|
|
197
|
-
runCommandHook(mode);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
202
|
-
const modulePath = fileURLToPath(import.meta.url);
|
|
203
|
-
|
|
204
|
-
if (invokedPath && path.basename(invokedPath) === path.basename(modulePath)) {
|
|
205
|
-
main();
|
|
206
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# Codex Parallel Reasoning
|
|
2
|
-
|
|
3
|
-
Use this reference only when `implement-spec` runs in Codex parallel mode and calls Codex `spawn_agent`.
|
|
4
|
-
|
|
5
|
-
Set worker `reasoning_effort` lower than the parent orchestrator:
|
|
6
|
-
|
|
7
|
-
| Orchestrator reasoning | Worker reasoning |
|
|
8
|
-
|------------------------|------------------|
|
|
9
|
-
| `xhigh` | `high` |
|
|
10
|
-
| `high` | `medium` |
|
|
11
|
-
| `medium` | `low` |
|
|
12
|
-
| `low` | `low` |
|
|
13
|
-
|
|
14
|
-
Rules:
|
|
15
|
-
|
|
16
|
-
- This policy is Codex-only. Do not apply it to other models or hosts.
|
|
17
|
-
- Pass the mapped value as `reasoning_effort` in each Codex `spawn_agent` call.
|
|
18
|
-
- Keep orchestration, retries, dependency decisions, and acceptance audit in the parent.
|
|
19
|
-
- If a worker task needs equal or higher reasoning, keep that task in the parent instead of spawning it.
|