autonomous-sdlc-harness 0.1.0
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/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +24 -0
- package/dist/cli.js +194 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.js +561 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/daemon.js +791 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/doctor.js +336 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.js +2023 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/registry.js +42 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/config/check.js +505 -0
- package/dist/config/check.js.map +1 -0
- package/dist/config/io.js +177 -0
- package/dist/config/io.js.map +1 -0
- package/dist/config/model.js +406 -0
- package/dist/config/model.js.map +1 -0
- package/dist/core/errors.js +71 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +537 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/json.js +125 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/layerCoverage.js +141 -0
- package/dist/core/layerCoverage.js.map +1 -0
- package/dist/core/layerGapRemedy.js +62 -0
- package/dist/core/layerGapRemedy.js.map +1 -0
- package/dist/core/nameList.js +23 -0
- package/dist/core/nameList.js.map +1 -0
- package/dist/core/paths.js +153 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/prompt.js +206 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/repoPaths.js +55 -0
- package/dist/core/repoPaths.js.map +1 -0
- package/dist/core/report.js +150 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/templating.js +88 -0
- package/dist/core/templating.js.map +1 -0
- package/dist/core/writer.js +479 -0
- package/dist/core/writer.js.map +1 -0
- package/dist/daemon/backend.js +180 -0
- package/dist/daemon/backend.js.map +1 -0
- package/dist/daemon/units.js +380 -0
- package/dist/daemon/units.js.map +1 -0
- package/dist/detect/nestedApplication.js +79 -0
- package/dist/detect/nestedApplication.js.map +1 -0
- package/dist/detect/presets.js +2033 -0
- package/dist/detect/presets.js.map +1 -0
- package/dist/detect/signals.js +1368 -0
- package/dist/detect/signals.js.map +1 -0
- package/dist/doctor/checks.js +3530 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/generators/claudeContext.js +588 -0
- package/dist/generators/claudeContext.js.map +1 -0
- package/dist/generators/githooks.js +446 -0
- package/dist/generators/githooks.js.map +1 -0
- package/dist/generators/harnessConfig.js +632 -0
- package/dist/generators/harnessConfig.js.map +1 -0
- package/dist/generators/notifications.js +191 -0
- package/dist/generators/notifications.js.map +1 -0
- package/dist/generators/outerLoopScripts.js +165 -0
- package/dist/generators/outerLoopScripts.js.map +1 -0
- package/dist/generators/permissionProfile.js +1172 -0
- package/dist/generators/permissionProfile.js.map +1 -0
- package/dist/generators/projectSettings.js +322 -0
- package/dist/generators/projectSettings.js.map +1 -0
- package/dist/generators/repoRoot.js +417 -0
- package/dist/generators/repoRoot.js.map +1 -0
- package/dist/generators/scripts.js +557 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/generators/stateDir.js +221 -0
- package/dist/generators/stateDir.js.map +1 -0
- package/dist/machine/paths.js +111 -0
- package/dist/machine/paths.js.map +1 -0
- package/dist/machine/plugins.js +224 -0
- package/dist/machine/plugins.js.map +1 -0
- package/dist/machine/registry.js +330 -0
- package/dist/machine/registry.js.map +1 -0
- package/package.json +23 -0
- package/scripts/README.md +13 -0
- package/scripts/daemon/launchd.plist.template +59 -0
- package/scripts/daemon/systemd.service.template +58 -0
- package/templates/README.md +15 -0
- package/templates/claude/CLAUDE.md +54 -0
- package/templates/claude/README.md +5 -0
- package/templates/claude/context/api.md +29 -0
- package/templates/claude/context/conventions.md +23 -0
- package/templates/claude/context/data-layer.md +28 -0
- package/templates/claude/context/data-storage.md +29 -0
- package/templates/claude/context/docs-catalog.md +29 -0
- package/templates/claude/context/domain.md +28 -0
- package/templates/claude/context/layer.md +20 -0
- package/templates/claude/context/module.md +30 -0
- package/templates/claude/context/package.md +29 -0
- package/templates/claude/context/presentation.md +32 -0
- package/templates/claude/context/state-slices.md +28 -0
- package/templates/claude/context/tests.md +28 -0
- package/templates/claude/harness-task-offer.md +58 -0
- package/templates/claude/push-notify.env.example +21 -0
- package/templates/claude/qa-accounts.env.example +38 -0
- package/templates/claude/qa_test_scenarios.md +110 -0
- package/templates/claude/settings.autonomous.json +93 -0
- package/templates/claude/settings.autonomous.qa.json +36 -0
- package/templates/githooks/README.md +3 -0
- package/templates/githooks/pre-push +72 -0
- package/templates/repo/README.md +3 -0
- package/templates/repo/gitattributes +16 -0
- package/templates/repo/gitignore +61 -0
- package/templates/repo/gitignore.qa +25 -0
- package/templates/repo/mcp.json +17 -0
- package/templates/scripts/README.md +5 -0
- package/templates/scripts/autonomous-format-stream.sh +95 -0
- package/templates/scripts/autonomous-notify.sh +337 -0
- package/templates/scripts/autonomous-watcher.sh +3087 -0
- package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
- package/templates/scripts/commit-on-branch.sh +288 -0
- package/templates/scripts/create-worktree.sh +360 -0
- package/templates/scripts/deploy.sh +47 -0
- package/templates/scripts/lib/harness-run-lib.sh +1481 -0
- package/templates/scripts/push-branch.sh +140 -0
- package/templates/scripts/refresh-branch.sh +244 -0
- package/templates/scripts/restart-watcher.sh +401 -0
- package/templates/scripts/scratch-run.sh +302 -0
- package/templates/scripts/setup-worktree.sh +262 -0
- package/templates/scripts/start-dev-server.sh +99 -0
- package/templates/scripts/test.sh +50 -0
- package/templates/scripts/typecheck.sh +50 -0
- package/templates/state-dir/README-root.md +13 -0
- package/templates/state-dir/README.md +9 -0
- package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
- package/templates/state-dir/architecture_reviews/README.md +9 -0
- package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
- package/templates/state-dir/autonomous_inbox/README.md +9 -0
- package/templates/state-dir/autonomous_logs/README.md +9 -0
- package/templates/state-dir/branch_statistics/README.md +9 -0
- package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
- package/templates/state-dir/clarification_digests/README.md +9 -0
- package/templates/state-dir/clarifications/README.md +9 -0
- package/templates/state-dir/code_reviews/README.md +9 -0
- package/templates/state-dir/dispatch_additions/README.md +19 -0
- package/templates/state-dir/docs_catalog/README.md +9 -0
- package/templates/state-dir/flow_progress/README.md +9 -0
- package/templates/state-dir/improvement_observations/README.md +19 -0
- package/templates/state-dir/improvement_suggestions.md +29 -0
- package/templates/state-dir/lessons.md +23 -0
- package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
- package/templates/state-dir/qa_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_reviews/README.md +9 -0
- package/templates/state-dir/scratch/README.md +11 -0
- package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_reviews/README.md +9 -0
- package/templates/state-dir/story_plans/README.md +9 -0
- package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/task_plan_reviews/README.md +9 -0
- package/templates/state-dir/task_plans/README.md +9 -0
- package/templates/state-dir/task_prompts/README.md +9 -0
- package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
- package/templates/state-dir/ui_test_plans/README.md +9 -0
- package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/user_reviews/README.md +9 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"_README": [
|
|
4
|
+
"UNATTENDED SETTINGS PROFILE - written by `autonomous-sdlc-harness init`, and yours from there on: edit it freely, a re-run keeps your copy. It is selected per run with `--settings .claude/settings.autonomous.json` and is never installed as the interactive default, so dropping that flag reverts every grant below with nothing to undo.",
|
|
5
|
+
"COMPLETENESS IS LOAD-BEARING: in print mode a tool that is neither allowed nor denied stalls rather than prompting, so `allow` has to cover the whole flow end to end. A gap surfaces as a hang, not as an error - add the missing entry here rather than relaxing the permission mode.",
|
|
6
|
+
"SCRIPT PATHS ARE UNQUOTED LITERALS: the guard matches the raw command string, so a quoted path fails a `*.sh` match, falls through to a prompt and stalls an unattended run. Never put quotes around one of the wrapper paths below.",
|
|
7
|
+
"ONE COMMAND PER ENTRY, NEVER A COMPOUND: a single `mkdir && cp && rm` block is refused where each of those commands succeeds on its own, because the joined string matches no entry. Keep every entry a single command.",
|
|
8
|
+
"THE CHECKOUT AND ITS SIBLING WORKTREES BOTH: every file glob and every wrapper path is listed for {{repoRoot}} and for the sibling pattern {{worktreeGlob}} under {{workRoot}}. A run in a second working copy that matches neither does not fail loudly - it silently skips whatever phase needed the path. A single-checkout repository simply has no sibling for the second entry to match, which costs nothing.",
|
|
9
|
+
"EVERY WRAPPER IN {{scriptsDir}} IS LISTED IN ALL THREE FORMS A CALLER MAY USE: the repo-relative invocation `commands.*` holds and an agent runs verbatim from the repository root, its repo-root-absolute twin for a caller that resolves the path first, and its sibling-worktree twin. Listing only one form leaves the other two matching neither `allow` nor `deny`, which is the stall this profile exists to prevent. THE DEPLOY WRAPPER IS ONE OF THEM, SO ITS THREE FORMS ARE IN `allow` HERE AND AGAIN IN `ask` BELOW: `ask` is evaluated before `allow` and returns first, so the ask is what a deploy hits and the allow rows never decide it. Deleting the `ask` deploy rows as duplicates therefore leaves a deploy an unattended run makes on its own - remove deploy from both blocks or from neither.",
|
|
10
|
+
"THE OUTER-LOOP SCRIPTS AN AGENT RUNS ARE LISTED THE SAME THREE WAYS, AND THE REST GET NOTHING: `autonomous-sdlc-harness init` writes a second family of scripts into {{scriptsDir}} whose bodies ship fixed and read `harness.config.json` at run time rather than carrying a command line of yours. Only the ones a dispatched agent is itself the thing that runs are listed here, and they are in `allow` rather than `ask` because they sit on the unattended commit path, where an `ask` is evaluated first and parks the run with nothing to answer it. The run watcher, the worktree tooling and the cleanup sweep are started by the watcher process or by you, and they carry no entry on purpose: one would hand a dispatched agent a path to a branch deletion or a daemon restart. Adding an entry for one of them is a decision to let an agent run it - make it deliberately, in the same three forms as everything else here.",
|
|
11
|
+
"THE PACKAGE MANAGER, THE BUILD AND THE DEPENDENCY INSTALL ARE DELIBERATELY NOT ALLOW-LISTED: `commands.build` and `commands.depInstall` are raw command lines with no wrapper, because they are run by a person or by the outer loop rather than by an agent, and granting an agent a package manager grants it every script that package manager can run. An adopter who wants one of them agent-run adds a wrapper script for it and one literal entry here, in the same three forms as the wrappers above. That stance leaves an ad-hoc probe a route, and it needs no entry here either - the next paragraph is that route, and the trade it makes rather than an exception it satisfies.",
|
|
12
|
+
"THE PROBE ROUTE GRANTS AN INTERPRETER AT ONE REMOVE, AND IT IS BROADER THAN THE GRANT ABOVE REFUSES, NOT NARROWER: an ad-hoc probe or a mutation check is written into the run-artifact tree's `scratch/` directory and run through `bash {{scriptsDir}}/scratch-run.sh`, an outer-loop script whose three forms are already listed above. It runs a FILE from that one directory rather than a string on the command line, which fences WHICH FILE runs and nothing about what that file does: the file is one the agent composed a tool call earlier, it runs with the session's own privileges, and it reaches everything the `deny` floor below, the `ask` list below and the plugin guard's own deny list withhold from a COMMAND STRING. The trade is deliberate and is recorded here rather than left to be discovered: the alternative is a run with no permitted way to execute anything, which silently downgrades a verification claim from executed to reasoned. What still holds for a subprocess is the `pre-push` git hook, because git runs it however git was invoked. Removing the route means deleting `scratch-run.sh`'s `agentInvocable` row in the generator and adding its basename to the plugin guard's deny list - both, since either alone leaves it reachable.",
|
|
13
|
+
"CREATING A FILE AND CHANGING ONE ARE TWO DIFFERENT GRANTS: `Edit` covers a change to a file that already exists, `Write` covers creating one - and almost every artifact a run produces is a new file, from a plan to a review to a ledger entry to whatever source file an implementer adds. Dropping the `Write` pair therefore leaves the most common operation in the whole flow matching neither `allow` nor `deny`, which in print mode is a stall and not a refusal. `defaultMode` does not close that gap: its auto-accept is scoped to the session's own working directory, and the entry that matters most here is the sibling-worktree one.",
|
|
14
|
+
"EVERY FILE RULE MARKS AN ABSOLUTE PATH WITH A `//` PREFIX - `Read`, `Edit` AND `Write` ALIKE - AND THE PATH BRINGS ITS OWN LEADING SLASH: the entries `Read(/{{repoRoot}}/**)`, `Edit(/{{repoRoot}}/**)` and `Write(/{{repoRoot}}/**)` above are that prefix immediately followed by the absolute path {{repoRoot}}, which is why each carries two slashes and not three. One tool syntax governs all three: a rule content starting with `//` is anchored at the filesystem root, while a single leading `/` is resolved relative to the root of the settings file the rule came from, so a one-slash rule names a path nobody has and matches no file. Adding a slash of its own to a path that already starts with one gives the same silent nothing from the other side.",
|
|
15
|
+
"NO `hooks` KEY, DELIBERATELY: the harness plugin ships its own guard hooks; they fire from the plugin, resolve their own root and append to whatever hooks you already have. Writing them here as well would run each guard twice and would replace your own hook configuration. This omission is a decision, not an oversight.",
|
|
16
|
+
"`deny` IS A FLOOR, NOT A PREFERENCE: it is evaluated before `allow` and cannot be overridden by one, which is what makes a broad `allow` beside it safe - the destructive commands below stay refused however widely the rest is granted. Add to it freely; do not weaken it, and do not try to re-enable one of its entries by adding an allow rule, because the allow will not win. What a deny entry holds back is what its pattern literally matches: each is a PREFIX match anchored at the START of the command string, so `Bash(git branch -D:*)` refuses `git branch -D <x>` and does not fire on the same words inside a compound such as `git status && git branch -D <x>`. It does not fire on `git -C <dir> branch -D <x>` either - the directory sits ahead of the words the pattern anchors on. Both gaps are closed on the plugin side rather than here - the guard hooks that auto-allow a compound, or a single directory-scoped statement, refuse to vouch for a piece that deletes, renames or overwrites a branch ref, whichever safe prefix it otherwise matches, and they parse the `-C` form rather than matching it as a prefix - so keep the two in step when you add an entry that names one of those prefixes. It is also why no `Bash(git -C:*)` entry is added above: it would grant the `-C` spelling of every verb this floor refuses, with no deny entry able to fire on it.",
|
|
17
|
+
"NO BROWSER-NAMESPACE ENTRY IN `deny`: a deny is evaluated before any allow and cannot be overridden, so denying the browser-automation namespaces would revoke the interactive-test agent's own grant along with everyone else's. That closure is done per agent, by the `tools:` allowlist on each agent definition - the one agent that drives a browser lists those tools and no other agent does."
|
|
18
|
+
],
|
|
19
|
+
"permissions": {
|
|
20
|
+
"allow": [
|
|
21
|
+
"Bash(git status:*)",
|
|
22
|
+
"Bash(git log:*)",
|
|
23
|
+
"Bash(git diff:*)",
|
|
24
|
+
"Bash(git show:*)",
|
|
25
|
+
"Bash(git ls-files:*)",
|
|
26
|
+
"Bash(git rev-parse:*)",
|
|
27
|
+
"Bash(git rev-list:*)",
|
|
28
|
+
"Bash(git branch:*)",
|
|
29
|
+
"Bash(git remote get-url:*)",
|
|
30
|
+
"Bash(git worktree:*)",
|
|
31
|
+
"Bash(git fetch:*)",
|
|
32
|
+
"Bash(git add:*)",
|
|
33
|
+
"Bash(git stash:*)",
|
|
34
|
+
"Bash(git commit:*)",
|
|
35
|
+
"Bash(ls:*)",
|
|
36
|
+
"Bash(cat:*)",
|
|
37
|
+
"Bash(grep:*)",
|
|
38
|
+
"Bash(find:*)",
|
|
39
|
+
"Bash(sed:*)",
|
|
40
|
+
"Bash(awk:*)",
|
|
41
|
+
"Bash(head:*)",
|
|
42
|
+
"Bash(tail:*)",
|
|
43
|
+
"Bash(wc:*)",
|
|
44
|
+
"Bash(jq:*)",
|
|
45
|
+
"Bash(mkdir -p:*)",
|
|
46
|
+
"Bash(pwd)",
|
|
47
|
+
"Bash(echo:*)",
|
|
48
|
+
"Edit(/{{repoRoot}}/**)",
|
|
49
|
+
"Edit(/{{worktreeGlob}}/**)",
|
|
50
|
+
"Write(/{{repoRoot}}/**)",
|
|
51
|
+
"Write(/{{worktreeGlob}}/**)",
|
|
52
|
+
"Read(/{{repoRoot}}/**)",
|
|
53
|
+
"Read(/{{worktreeGlob}}/**)",
|
|
54
|
+
"Bash({{scriptInvocation}}:*)",
|
|
55
|
+
"Bash(bash {{scriptsDirAbs}}/{{scriptFile}}:*)",
|
|
56
|
+
"Bash(bash {{worktreeGlob}}/{{scriptPath}}:*)",
|
|
57
|
+
"Bash({{outerLoopInvocation}}:*)",
|
|
58
|
+
"Bash(bash {{scriptsDirAbs}}/{{outerLoopFile}}:*)",
|
|
59
|
+
"Bash(bash {{worktreeGlob}}/{{outerLoopPath}}:*)"
|
|
60
|
+
],
|
|
61
|
+
"deny": [
|
|
62
|
+
"Bash(rm -rf:*)",
|
|
63
|
+
"Bash(git branch -D:*)",
|
|
64
|
+
"Bash(git branch -d:*)",
|
|
65
|
+
"Bash(git branch -M:*)",
|
|
66
|
+
"Bash(git branch -m:*)"
|
|
67
|
+
],
|
|
68
|
+
"ask": [
|
|
69
|
+
"Bash(git checkout:*)",
|
|
70
|
+
"Bash(git pull:*)",
|
|
71
|
+
"Bash(git push:*)",
|
|
72
|
+
"Bash(git reset:*)",
|
|
73
|
+
"Bash(git merge:*)",
|
|
74
|
+
"Bash(git rebase:*)",
|
|
75
|
+
"Bash(git restore:*)",
|
|
76
|
+
"Bash(git revert:*)",
|
|
77
|
+
"Bash(git cherry-pick:*)",
|
|
78
|
+
"Bash(git tag:*)",
|
|
79
|
+
"Bash(git rm:*)",
|
|
80
|
+
"Bash(git mv:*)",
|
|
81
|
+
"Bash(git clean:*)",
|
|
82
|
+
"Bash(git remote add:*)",
|
|
83
|
+
"Bash(git remote remove:*)",
|
|
84
|
+
"Bash(git remote set-url:*)",
|
|
85
|
+
"Bash({{deployInvocation}}:*)",
|
|
86
|
+
"Bash(bash {{scriptsDirAbs}}/{{deployFile}}:*)",
|
|
87
|
+
"Bash(bash {{worktreeGlob}}/{{deployPath}}:*)"
|
|
88
|
+
],
|
|
89
|
+
"defaultMode": "acceptEdits",
|
|
90
|
+
"additionalDirectories": ["{{stateDirAbs}}"]
|
|
91
|
+
},
|
|
92
|
+
"model": "{{agentModel}}"
|
|
93
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_README": [
|
|
3
|
+
"THE BROWSER HALF, ADDED ONLY WHEN TWO CONDITIONS BOTH HOLD - THE INTERACTIVE TEST PHASE IS ENABLED, AND ITS DRIVER IS THE BROWSER ONE: the `enabledMcpjsonServers` key, the `mcp__` entries at the end of `allow` and the port and liveness probes beside them were merged in because `phases.qa` was true AND `qa.driver` was `web-playwright` when this file was generated. Fail either condition and none of them is written at all, so an adopter who never drives a browser pays neither the browser tool schemas in every session's context nor a launched browser process. With a MOBILE driver that is not merely a saving: the mobile variants of the interactive test agent are declared-not-implemented in this release and carry built-ins-only tool allowlists, so a server started here would be one no agent could ever call - which is why the repository's own `.mcp.json` is withheld in the same run rather than left declaring servers this profile never starts. Turning the phase on in `harness.config.json`, or changing `qa.driver` to `web-playwright`, and re-running `autonomous-sdlc-harness init` does NOT rewrite this file — it is yours once written; copy the entries in from a fresh generation, or re-run it with --force, which takes a .bak first.",
|
|
4
|
+
"BOTH KEYS ARE NEEDED AND THEY DO DIFFERENT THINGS: `enabledMcpjsonServers` STARTS the browser servers the repository's own MCP wiring declares, while the `mcp__` entries in `allow` gate which of their tools may run. A fresh working copy has no local settings file to enable a server in, and this profile is the only settings source an unattended run loads - without the enablement the servers never start, the pre-allowed tools do not EXIST, and the phase stalls rather than failing, because an un-loaded tool in print mode stalls instead of prompting. Enabling a server is not itself a capability grant: removing a tool entry below removes the capability while leaving the server running.",
|
|
5
|
+
"THE ALLOWED TOOL SET IS THE ONE THE PHASE DRIVES - navigation, snapshot, console, network, click, type, select, wait, screenshot, tracing and one client-storage escape hatch - and nothing wider. That last one is `mcp__playwright__browser_evaluate`, and it is in the base set because a test that has to start from a known client-storage state has no other route to one: the interactive-test agent grants it in its own `tools:` allowlist and documents the single way it goes wrong (an unbounded `await` inside it hangs, because the tool carries no timeout of its own), and withholding it does not make a test safer - it makes the phase STALL at that test's first step, per the completeness line above. One tool that agent may call is deliberately NOT here: `mcp__playwright__browser_route`, which fakes the network the application under test talks to. Granting an agent that is an adopter's decision rather than a default - add the entry if your interactive tests stub backend responses, and until you do, a test that reaches for it stalls rather than failing. Any other tool a test needs gets one more entry here rather than a namespace wildcard, because these entries are what an adopter reads to know what the phase can do to a running application.",
|
|
6
|
+
"THE PORT PROBE IS ALLOW-LISTED AS A COMMAND PREFIX RATHER THAN AS ONE PORT: the phase serves its own instance on the configured port seed and moves past it when that port is taken, so an entry naming a single port would leave the very next probe matching neither `allow` nor `deny`. The dev-server wrapper itself is not re-declared here - it is one of the wrapper scripts in `allow` above, listed there in all three forms a caller may use, and a second declaration of it here is how the two spellings drift apart. THE LIVENESS PROBE BESIDE IT IS THE SAME CLASS, AND IT IS `ps -p` RATHER THAN `ps` ON PURPOSE: it exists for the one step that owns the dev server - which confirms the process it started has not exited before it trusts a poll that answered, because a URL answering on a port says nothing about WHOSE server answered - so the entry grants a query about one pid and not a listing of every process on the machine; that step grades it by EXIT STATUS and never by parsing its output, which is what keeps a platform-dependent output shape out of the contract; it sits in this fragment rather than in the base profile, so a repository that never drives the phase is not widened by it; and this file is written create-if-absent, so a fragment generated before this line existed does not gain it on a plain re-run - add the one line by hand, or take it from a --force run, which writes a .bak first; the bare `ps -p <pid>` form has been measured to run even on a profile carrying no `ps` entry at all, so this line is the guarantee rather than the only route, and the phase's own E.0 step 3 is where what to do with a liveness check that cannot run is stated.",
|
|
7
|
+
"THE PHASE'S OWN HELPER SCRIPTS ARE NOT ALLOW-LISTED HERE - ADD THEM BY HAND, ONCE: the port, dev-server and QA-user helpers this phase runs ship inside the harness plugin and are invoked as `bash ${CLAUDE_PLUGIN_ROOT}/scripts/<name>.sh`. Keeping the token in an entry rescues nothing, because the guard matches the raw command string and a substitution anywhere in that string defeats the match however the entry is worded (measured against `claude` 2.1.227, with the limits of that measurement recorded in the harness repository's own `docs/development.md` section 3). So each line you add must name a RESOLVED root - `Bash(bash <resolved plugin root>/scripts/<name>.sh:*)`, in the same unquoted form the wrapper entries above use, naming the script FILE rather than the directory holding it, because a directory-prefix entry was measured not to match either. That form is what a resolved command meets, whatever kind of file names the helper - an adoption run on 2026-08-26 reached three of them through it and stalled at none - but WHICH root it resolves to does depend on that: a helper named in an instruction file is resolved by the agent itself, one named in an agent definition body has the token substituted by the runtime, and on a directory-sourced marketplace those two were measured to be different directories. Add the line once per helper per root where they differ. `autonomous-sdlc-harness init` does not generate these lines, and an unknowable root is not the reason - it is recorded per plugin in the agent runner's own `installed_plugins.json`. What stops the generator is that `init` may run before the plugin is enabled, and that the path carries the plugin VERSION, so a line written once goes silently stale on the next upgrade. The resolving is therefore done at check time instead: run `npx autonomous-sdlc-harness doctor` and paste what its `plugin-permissions` check prints - it re-resolves both roots on every run and names each missing line exactly, grouped by root. The helper names are declared once, in the plugin's own `scripts/README.md`, and are deliberately not repeated here. Until those lines exist the phase STALLS rather than failing, per the completeness line above. This file is written create-if-absent, so the lines you add survive every later `autonomous-sdlc-harness init`: a plain re-run keeps the whole file, and `--force`, the one run that regenerates it from the template after a .bak, re-reads it first and CARRIES FORWARD every `permissions.allow` entry naming a plugin root this machine still resolves - the lines above among them. The one it does not carry is an entry naming a root it no longer resolves, which is what a plugin upgrade leaves behind: that run names each entry it dropped, and `doctor` names them on every run after it. That exception applies only where the plugin DOES resolve on the machine running it - where none of its roots resolve at all, nothing there can tell a stranded entry from one you pasted a minute ago, so the run carries every absolute entry outside the checkout forward UNVERIFIED and says so in those words."
|
|
8
|
+
],
|
|
9
|
+
"enabledMcpjsonServers": ["playwright", "chrome-devtools"],
|
|
10
|
+
"permissions": {
|
|
11
|
+
"allow": [
|
|
12
|
+
"mcp__playwright__browser_navigate",
|
|
13
|
+
"mcp__playwright__browser_snapshot",
|
|
14
|
+
"mcp__playwright__browser_console_messages",
|
|
15
|
+
"mcp__playwright__browser_network_requests",
|
|
16
|
+
"mcp__playwright__browser_network_request",
|
|
17
|
+
"mcp__playwright__browser_click",
|
|
18
|
+
"mcp__playwright__browser_type",
|
|
19
|
+
"mcp__playwright__browser_select_option",
|
|
20
|
+
"mcp__playwright__browser_wait_for",
|
|
21
|
+
"mcp__playwright__browser_take_screenshot",
|
|
22
|
+
"mcp__playwright__browser_evaluate",
|
|
23
|
+
"mcp__chrome-devtools__navigate_page",
|
|
24
|
+
"mcp__chrome-devtools__take_snapshot",
|
|
25
|
+
"mcp__chrome-devtools__click",
|
|
26
|
+
"mcp__chrome-devtools__fill",
|
|
27
|
+
"mcp__chrome-devtools__fill_form",
|
|
28
|
+
"mcp__chrome-devtools__wait_for",
|
|
29
|
+
"mcp__chrome-devtools__performance_start_trace",
|
|
30
|
+
"mcp__chrome-devtools__performance_stop_trace",
|
|
31
|
+
"mcp__chrome-devtools__performance_analyze_insight",
|
|
32
|
+
"Bash(lsof -ti:*)",
|
|
33
|
+
"Bash(ps -p:*)"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# githooks/
|
|
2
|
+
|
|
3
|
+
The caller-agnostic pre-push guard `init` writes into the configured `githooksDir` before pointing `core.hooksPath` at that directory. The guard refuses a push to a protected branch whoever invokes it — an agent, a wrapper script, or a human at a terminal — which is what makes it a last line of defence rather than a convention. That is caller-agnosticism *within git*, and the whole of the claim: git runs the hook on every push **git** performs, so a tool that implements push against the git backend itself, as `jj git push` does, performs no git push and runs no git hook. `doctor`'s `jj-repository` check reports that where it applies, and names what covers such a push: the session-level `PreToolUse` protected-branch guard, which sees it and refuses one whose target is **named**, and the forge-side branch ruleset, which is the only true floor and the only cover for a `jj git push` that names no bookmark — that form carries no target for the guard's explicit-target test and cannot trip its HEAD test either, since jj leaves `HEAD` detached. The set it protects is the adopter's: `protectedBranches` (else that key's schema default) unioned with `defaultBranch`, the same set `hr_protected_patterns` in the shared script library resolves for the git wrappers. The two differ in *when*: the wrappers re-read `harness.config.json` on every call, while the hook is `create-if-absent` and carries the set substituted into its `case` label when `init` wrote it, so an edit to `protectedBranches` reaches the hook only at `init --force` (or after deleting the hook and re-running `init`) and until then the two enforce different sets. A third run re-renders it as a *consequence* rather than as a way to change the set: `init --reset-config` rebuilds `harness.config.json` itself, and re-renders the hook from the rebuilt file when the `case` label the hook carried no longer matches the set that file resolves — after a `.bak`, and never when the label cannot be read. It is not a route to reach for deliberately: the rebuild takes the whole config from detection and the flags, so every hand-set value goes with it. A push is judged by the `case` label in the written file, because that is the file git runs. Roadmap item 13 owns the writer.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Caller-agnostic pre-push BACKSTOP, written by `autonomous-sdlc-harness init` into the
|
|
3
|
+
# configured `githooksDir` - which init then points `core.hooksPath` at, so the hook
|
|
4
|
+
# travels with the checkout. Yours from there on: edit it freely, a re-run keeps your copy.
|
|
5
|
+
#
|
|
6
|
+
# WHAT IT DOES: refuses a push whose TARGET ref is a protected branch, whoever invoked git -
|
|
7
|
+
# a person at a terminal, a wrapper script, or an unattended run - because git itself runs
|
|
8
|
+
# this hook on every push GIT PERFORMS, including one started by a subprocess of some other
|
|
9
|
+
# program. A guard that inspects the command an agent submitted sees only that one string,
|
|
10
|
+
# so a push spawned indirectly never re-enters it. This hook closes that gap, which is what
|
|
11
|
+
# makes it a last line of defence rather than a convention. Caller-agnostic WITHIN GIT is
|
|
12
|
+
# the whole of that claim, and the fourth bypass below is the caller it does not reach.
|
|
13
|
+
#
|
|
14
|
+
# IT IS HONESTLY BYPASSABLE ON-BOX - `git push --no-verify`, re-pointing or unsetting
|
|
15
|
+
# core.hooksPath, deleting this file, and pushing with a tool that implements push against
|
|
16
|
+
# the git backend itself instead of running git (`jj git push` in a colocated jj repository
|
|
17
|
+
# performs no git push and therefore runs no git hook - measured on one repository, one
|
|
18
|
+
# hook, seconds apart) all skip it - so it is a backstop, not a floor. The only true floor
|
|
19
|
+
# is a rule enforced by the host the repository is pushed to. This hook is what protects the
|
|
20
|
+
# branch until there is one, and once there is one it is what reports the refusal locally
|
|
21
|
+
# rather than after a round trip.
|
|
22
|
+
#
|
|
23
|
+
# THE PROTECTED SET COMES FROM CONFIGURATION, not from a list baked into this script: the
|
|
24
|
+
# `case` pattern below was substituted, when init wrote this file, from `protectedBranches`
|
|
25
|
+
# in harness.config.json (or that key's schema default when it is absent) UNIONED WITH
|
|
26
|
+
# `defaultBranch` - so the branch work merges back into is in the set whatever the list
|
|
27
|
+
# says, and an empty list means exactly that one branch. `case` patterns are globs, so one
|
|
28
|
+
# entry like `release/*` protects a whole namespace. TO CHANGE THE SET: edit the `case`
|
|
29
|
+
# pattern below and put the same set in `protectedBranches` - or edit that key and re-run
|
|
30
|
+
# `init --force`, which writes the hook again from the config in effect after copying this
|
|
31
|
+
# file to a .bak beside it, so the old set stays readable there. `--force` overwrites
|
|
32
|
+
# generated files but never harness.config.json, which init reads on every run, so a set
|
|
33
|
+
# that lives only in that file is the set this hook is re-rendered from. Without `--force`,
|
|
34
|
+
# delete this file and re-run `init`: a re-run keeps a hook that is already here, so
|
|
35
|
+
# deleting it is the part that makes the re-run re-render it. A third run re-renders this
|
|
36
|
+
# file as a CONSEQUENCE rather than as a way to change the set - `init --reset-config`
|
|
37
|
+
# rebuilds harness.config.json itself, and re-renders this hook from the rebuilt file when
|
|
38
|
+
# the `case` label here no longer matches the set that file resolves, after copying this
|
|
39
|
+
# file to a .bak beside it and never when the label cannot be read at all. Do not reach for
|
|
40
|
+
# it to change the set: it rebuilds the whole config from detection and the flags, so every
|
|
41
|
+
# hand-set value goes with it. Either way the union holds:
|
|
42
|
+
# leaving `defaultBranch` out of `protectedBranches` does not take it out of a re-rendered
|
|
43
|
+
# `case` pattern, so only a hand edit here can drop it and the next re-render puts it back.
|
|
44
|
+
# Never change only one of the two, because the set that is actually enforced on a push is
|
|
45
|
+
# the one in this file.
|
|
46
|
+
#
|
|
47
|
+
# GIT'S CONTRACT: the hook is invoked as `pre-push <remote-name> <remote-url>` and is fed
|
|
48
|
+
# one line per ref on stdin - `<local-ref> <local-sha> <remote-ref> <remote-sha>`. The check
|
|
49
|
+
# is on <remote-ref>, the push TARGET, with `refs/heads/` stripped: `HEAD:main`,
|
|
50
|
+
# `feature:refs/heads/main` and `origin main` all arrive as the same target ref on stdin, so
|
|
51
|
+
# matching that one field covers every spelling and the command line is never re-parsed.
|
|
52
|
+
#
|
|
53
|
+
# `set -uo pipefail`, and deliberately NOT `-e`: a glitch in this script has to fail SAFE
|
|
54
|
+
# toward allowing a legitimate push to an unprotected branch rather than blocking one.
|
|
55
|
+
|
|
56
|
+
set -uo pipefail
|
|
57
|
+
|
|
58
|
+
# The happy path is pure string matching on the stdin lines - no git subprocess at all.
|
|
59
|
+
# Empty stdin, or no protected target among the refs, falls through to the exit 0 below.
|
|
60
|
+
while read -r local_ref local_sha remote_ref remote_sha; do
|
|
61
|
+
# Strip the refs/heads/ prefix to get the bare target branch name.
|
|
62
|
+
target="${remote_ref#refs/heads/}"
|
|
63
|
+
|
|
64
|
+
case "$target" in
|
|
65
|
+
{{protectedBranchesCase}})
|
|
66
|
+
echo "pre-push: refusing to push to protected branch '$target' - land the change through a pull request" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
done
|
|
71
|
+
|
|
72
|
+
exit 0
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# repo/
|
|
2
|
+
|
|
3
|
+
The files `init` writes at the adopter's repository root, stored dot-less per the naming rule in the parent README: the git attributes and ignore templates, and the MCP server wiring for browser automation. `gitignore.qa` is a **fragment** of `gitignore` rather than a fourth file — the ignore rules for what the browser half of a run leaves in the working copy, rendered into the parent template only under the same `browserWiringApplies` gate as the MCP wiring, on the pattern `claude/settings.autonomous.qa.json` sets. It carries its own comments because they are gated with it: a repository that drives no browser must not carry prose explaining rules it does not have. Roadmap item 13 owns the writer. The MCP wiring template is written **only when the QA phase is enabled and its `qa.driver` is `web-playwright`**, so an adopter who never runs browser QA pays neither the browser tool schemas in every session's context nor a launched browser process — which is why it is a conditional write rather than part of the unconditional root set. A **mobile** driver gets a note and no file, for a second reason: the two mobile variants of the interactive test agent ship declared-not-implemented with built-ins-only tool allowlists, so a declared server would be wiring nothing can ever start. The permission profile's interactive-test fragment is gated on the same condition and in the same run — the profile names the servers it starts and this file declares them, so gating one side alone leaves the two disagreeing.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Written by `autonomous-sdlc-harness init`, and yours from there on: edit it freely, a
|
|
2
|
+
# re-run keeps your copy.
|
|
3
|
+
#
|
|
4
|
+
# Line-ending normalisation only. Everything else a repository puts in this file - diff
|
|
5
|
+
# drivers, merge strategies, linguist hints, filters - is a project decision the harness
|
|
6
|
+
# has no opinion about, so it starts empty and you add to it.
|
|
7
|
+
|
|
8
|
+
# Store every text file with LF whatever the contributing machine writes it with.
|
|
9
|
+
* text=auto
|
|
10
|
+
|
|
11
|
+
# ...and check the executable assets out with LF on every platform. A script is run by
|
|
12
|
+
# the interpreter named on its `#!` line, and a CR at the end of that line is read as
|
|
13
|
+
# part of the interpreter's name: a CRLF checkout fails with "bad interpreter" or
|
|
14
|
+
# "no such file or directory", neither of which mentions a line ending.
|
|
15
|
+
*.sh text eol=lf
|
|
16
|
+
{{githooksDir}}/* text eol=lf
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Machine-local files this repository names by path but never commits. Each path is a
|
|
2
|
+
# value in harness.config.json, so it is the same for everyone who clones, while the file
|
|
3
|
+
# it names never leaves the machine. Where one of these has a committed `.example`
|
|
4
|
+
# sibling, that sibling is what a fresh clone copies from. The client env file is the one whose
|
|
5
|
+
# DIRECTORY is the configured value (`appDir`) while its name is fixed: setup-worktree.sh symlinks
|
|
6
|
+
# it into every working copy it bootstraps, so without this rule every fresh worktree opens with an
|
|
7
|
+
# untracked entry. What is ignored is the file and never the directory, so a committed
|
|
8
|
+
# `.env.example` beside it stays committable.
|
|
9
|
+
{{pushEnvPath}}
|
|
10
|
+
{{qaCredentialsPath}}
|
|
11
|
+
{{clientEnvPath}}
|
|
12
|
+
# The per-machine settings file the agent runner writes beside the committed profiles.
|
|
13
|
+
.claude/settings.local.json
|
|
14
|
+
# The presence-only marker one user's "don't ask again" answer writes to silence the change-request
|
|
15
|
+
# offer. It lives at the main worktree's root and is checked there from every worktree of this
|
|
16
|
+
# checkout, so one answer covers them all - its existence is the whole signal, so it has no
|
|
17
|
+
# contents to commit.
|
|
18
|
+
.claude/harness-no-offer
|
|
19
|
+
# The run daemon's transcripts, event logs and run registry are machine-local. The
|
|
20
|
+
# directory itself is committed, because its README is the contract for what belongs in
|
|
21
|
+
# it, so the contents are ignored and that one file is excepted - an ignore rule on the
|
|
22
|
+
# directory would take the README with it, and git cannot re-include a file whose parent
|
|
23
|
+
# directory is excluded.
|
|
24
|
+
{{logsGlob}}
|
|
25
|
+
{{logsReadmeException}}
|
|
26
|
+
# The unattended loop's stop, pause and dispatch-count control files, written flat at the root of
|
|
27
|
+
# the run-artifact tree while a run is in flight. The tree around them is committed, so one
|
|
28
|
+
# `git add` of it would take whichever exist at that moment - and a committed STOP halts every run
|
|
29
|
+
# for everyone who clones, at a step whose own instruction forbids deleting the file.
|
|
30
|
+
{{runControlArtifacts}}
|
|
31
|
+
# The questions a parked run asks and the answers it is given are machine-local, while the
|
|
32
|
+
# directory's README is the committed contract for the exchange - so the contents are ignored and
|
|
33
|
+
# that one file is excepted, exactly as for the log directory above. A question sits one level
|
|
34
|
+
# further down, under a <branch> subdirectory, which this glob covers with everything else beneath
|
|
35
|
+
# the directory. On a repository whose ignore file was generated before this pair existed: the
|
|
36
|
+
# managed block is only ever added to and never pruned, so the older whole-directory rule for this
|
|
37
|
+
# channel - the line ending `/clarifications/` - is still in the file and has to be DELETED BY HAND,
|
|
38
|
+
# and so do the two comment lines that stood above it, which state that this channel has no committed
|
|
39
|
+
# README and are wrong now that it has one. While that rule stands the exception cannot take effect,
|
|
40
|
+
# wherever the two sit relative to each other, because git cannot re-include a file whose parent
|
|
41
|
+
# directory is excluded - `doctor`'s `ignore-rules` check reports that state and names every line to
|
|
42
|
+
# delete.
|
|
43
|
+
{{clarificationsGlob}}
|
|
44
|
+
{{clarificationsReadmeException}}
|
|
45
|
+
# The drop point the unattended loop takes its work from. The prompts left in it are machine-local
|
|
46
|
+
# and its README is the committed contract for what belongs there, so the contents are ignored and
|
|
47
|
+
# that one file is excepted, exactly as for the log directory above.
|
|
48
|
+
{{inboxGlob}}
|
|
49
|
+
{{inboxReadmeException}}
|
|
50
|
+
# The throwaway files an agent runs a probe or a mutation check from. They are written to be executed
|
|
51
|
+
# once and deleted, never to be committed or read back, while the directory's README is the committed
|
|
52
|
+
# contract for that - so the contents are ignored and that one file is excepted, exactly as for the
|
|
53
|
+
# log directory above.
|
|
54
|
+
{{scratchGlob}}
|
|
55
|
+
{{scratchReadmeException}}
|
|
56
|
+
# The backup `config set` copies the configuration to before every write, and the one .bak an
|
|
57
|
+
# adopter gets without asking for it: applying a layer split is the documented path, so without this
|
|
58
|
+
# rule a wired repository carries an untracked file from its first `config set` onward. The leading
|
|
59
|
+
# slash anchors it at the repository root, the only place the configuration is written.
|
|
60
|
+
/{{configBackupFile}}
|
|
61
|
+
{{qaBrowserArtifacts}}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# What the browser half of a run leaves in the working copy. These rules and these comments are
|
|
2
|
+
# rendered only where a browser is actually driven - the interactive-test phase on AND its driver
|
|
3
|
+
# the browser one - because only that driver starts an MCP server, so a repository that drives none
|
|
4
|
+
# carries neither the rules nor prose explaining rules it does not have.
|
|
5
|
+
# The browser server's own output directory, created at the repository root: screenshots, traces and
|
|
6
|
+
# saved sessions. Ignored as a whole directory, unlike the run-artifact directories above - nothing
|
|
7
|
+
# commits a contract file into it, so there is no README a directory rule would take with it, and a
|
|
8
|
+
# re-run writes its own.
|
|
9
|
+
.playwright-mcp/
|
|
10
|
+
# The second pinned browser server writes no fixed project-local directory: it takes a temporary,
|
|
11
|
+
# auto-cleaned user-data-dir by default. This rule is a DEFENSIVE catch if one is ever pointed here
|
|
12
|
+
# (e.g. via an --output-dir flag at run time) rather than a directory the harness knows is written.
|
|
13
|
+
# It costs nothing while the directory never appears, and a re-run neither creates nor needs it.
|
|
14
|
+
.chrome-devtools-mcp/
|
|
15
|
+
# Where the interactive-test agent is told to send a screenshot. It sits inside the run-artifact
|
|
16
|
+
# tree, so an unattended run's launch check tolerates it as a <state_dir> artifact even if this rule
|
|
17
|
+
# is ever dropped. Machine-local evidence of one run: no contract file is committed under it, and a
|
|
18
|
+
# re-run takes its own screenshots.
|
|
19
|
+
{{qaArtifactsDir}}
|
|
20
|
+
# A screenshot the browser server wrote at the repository ROOT under a bare name, because it
|
|
21
|
+
# sanitized the filename it was given rather than honouring the subdirectory in it. DEFENSIVE in the
|
|
22
|
+
# same sense as the rule two above: it costs nothing while the file never appears, and a re-run
|
|
23
|
+
# neither creates nor needs it. The leading slash anchors it at the root and `.png` narrows it to the
|
|
24
|
+
# one artifact kind, so a committed `qa_`-named image in a subdirectory is untouched.
|
|
25
|
+
/qa_*.png
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Browser wiring for the interactive test phase, written by `autonomous-sdlc-harness init` and merged rather than overwritten on a re-run: a server you add here survives, and one of these two is re-added only if it is missing. THE TWO SERVERS ARE SEPARATE BROWSER INSTANCES - each launches and drives its own browser, so a page one of them has navigated or signed in is not visible to the other, and a test that needs both drives each of them into the state it needs. BOTH RUN --isolated because each package otherwise defaults to one fixed persistent profile directory: a second instance started against a profile already in use refuses to launch, which is a failure that surfaces only when two runs overlap. THE VERSIONS ARE PINNED DELIBERATELY - a browser-automation server's tool names and flags change between releases, and an unpinned fetch would change what the phase can do without any edit here. Raise a pin as its own change, and re-check the tool names allow-listed in the generated permission profile when you do. NEITHER PACKAGE IS INSTALLED BY init - the `npx -y` above fetches each pinned spec from the registry the first time the interactive-test phase runs, so a machine that is offline, behind a proxy, or on a mirror that does not carry those exact versions fails at that first dispatch rather than at setup; `npx autonomous-sdlc-harness doctor --check-registry` answers it beforehand.",
|
|
3
|
+
"mcpServers": {
|
|
4
|
+
"playwright": {
|
|
5
|
+
"type": "stdio",
|
|
6
|
+
"command": "npx",
|
|
7
|
+
"args": ["-y", "@playwright/mcp@0.0.75", "--isolated"],
|
|
8
|
+
"env": {}
|
|
9
|
+
},
|
|
10
|
+
"chrome-devtools": {
|
|
11
|
+
"type": "stdio",
|
|
12
|
+
"command": "npx",
|
|
13
|
+
"args": ["-y", "chrome-devtools-mcp@1.1.1", "--isolated"],
|
|
14
|
+
"env": {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# scripts/
|
|
2
|
+
|
|
3
|
+
The project-command wrapper scripts `init` writes into the configured `scriptsDir`: type-check, test and dev-server, configured through `commands.*` in `harness.config.json`, and deploy, configured through `deploy.command` on the separate `deploy` object — so no build tool and no hosting provider is hardcoded anywhere in the harness. They exist as scripts rather than as raw command lines because an unattended run's permission profile can allow-list a literal script path far more safely than an arbitrary command, which is why `commands.<key>` holds the **wrapper invocation** `bash <scriptsDir>/<name>.sh` — the literal the profile allow-lists — while the **raw** command line lives inside the script. Which raw line that is follows one precedence, and it is what keeps the pair from being circular: a raw command line already sitting in `commands.<key>` wins, because an adopter who edited it there meant it — and that state is **reported** rather than silently blessed, since the raw line is not the value the permission profile allow-lists: `init` names it as it writes the wrapper from it, `config set` names it as the value is stored, and `doctor`'s `command-wrappers` check grades it on every later run; otherwise, when that key holds this wrapper's own invocation — the normal state after a first `init` — the body is the line stack detection produced; and when neither resolves, the body names what to fix — the key to set, or for `deploy`, which detection never supplies, this script itself — and exits non-zero rather than appearing to have run a check it never ran. Two states get no wrapper at all, and therefore no allow entry: a key still holding the placeholder `init` writes for a command it could not detect, and `commands.typecheck` holding the `<none>` sentinel — the first is unfinished and is the one that asks the adopter to do something about it, the second is the answer that this repository has no such command and asks for nothing. **Each script prints its own verdict line**, so a caller never appends an exit-code probe to it: that compound form is exactly what stalls an unattended run on a permission prompt — except `start-dev-server.sh`, which has no verdict to give because it starts a long-running process. Every wrapper anchors itself to its own checkout before it runs and forwards whatever arguments it was given to the **last** command of its raw line — whether that command accepts a bare path or name filter is a property of the command, not of the wrapper, so a line ending in a sub-command's own flags takes none; `docs/cli.md` §5 is the full contract.
|
|
4
|
+
|
|
5
|
+
**The other family in this directory is not generated.** The run watcher, its restart wrapper, the notifier and its stream formatter, the commit / push / branch-refresh / worktree / cleanup wrappers, the scratch runner and the shared library at `lib/harness-run-lib.sh` are copied byte for byte into the same `scriptsDir`, because each reads `harness.config.json` *at run time* rather than carrying a value frozen in when `init` ran — a guard whose protected-branch set was baked into a file at generation time enforces the wrong set the moment that list changes, and does it silently. They therefore carry no `{{token}}` at all. `cli/src/generators/outerLoopScripts.ts` declares that set and marks which of its rows an agent may invoke; `cli/scripts/README.md` records why they live under `scriptsDir` rather than in the installed package. **One of them runs a file it is given:** `scratch-run.sh` runs an agent's language probe or mutation check in the interpreter that file's extension names, and refuses any argument that does not resolve inside `<state_dir>/scratch/`.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# autonomous-format-stream.sh — turn a headless agent run's `stream-json` events
|
|
3
|
+
# into ONE short line per meaningful event, so the run's log reads like an
|
|
4
|
+
# interactive session and stays tailable while the run is still going.
|
|
5
|
+
#
|
|
6
|
+
# WHO PIPES INTO IT. The watcher, and only the watcher: it starts the agent CLI
|
|
7
|
+
# with `--output-format stream-json --verbose`, tees the raw events to
|
|
8
|
+
# `<state_dir>/autonomous_logs/<branch>.stream.jsonl` (which the usage gate
|
|
9
|
+
# parses for `rate_limit_event`) and pipes the same stream through this filter
|
|
10
|
+
# into the human-readable run log. Nothing here writes a file, opens a network
|
|
11
|
+
# connection, reads configuration or takes an argument — stdin to stdout, once.
|
|
12
|
+
#
|
|
13
|
+
# WHAT IT DELIBERATELY DROPS, AND WHY. A raw transcript of a multi-hour run is
|
|
14
|
+
# tens of megabytes and unreadable, and the three biggest contributors carry the
|
|
15
|
+
# least operator value:
|
|
16
|
+
#
|
|
17
|
+
# * thinking blocks — unbounded, and the decision they lead to is in the text
|
|
18
|
+
# or the tool call that follows;
|
|
19
|
+
# * `tool_result` bodies — a file read is the whole file;
|
|
20
|
+
# * `system` / init envelopes — one-time setup noise.
|
|
21
|
+
#
|
|
22
|
+
# WHAT IT KEEPS is what answers "what is this run doing right now":
|
|
23
|
+
#
|
|
24
|
+
# * assistant text, which is where an orchestrator's `[A · Task N · …]`
|
|
25
|
+
# heartbeat line appears — trimmed, newlines folded to `⏎` so one event
|
|
26
|
+
# stays one line, and clipped to 300 characters;
|
|
27
|
+
# * `tool_use` dispatches: the tool name plus one short descriptor, taken from
|
|
28
|
+
# the first of `subagent_type` / `description` / `command` / `file_path`
|
|
29
|
+
# that the call carries, clipped to 160 characters;
|
|
30
|
+
# * the final `result` envelope: the subtype, the turn count, the cost and the
|
|
31
|
+
# four-way token split (input / output / cache-write / cache-read).
|
|
32
|
+
#
|
|
33
|
+
# A LINE THAT IS NOT JSON IS A NO-OP, BY DESIGN. `fromjson? // empty` swallows
|
|
34
|
+
# it. The stream this reads is interleaved with whatever the CLI writes to the
|
|
35
|
+
# same descriptor — a warning, a partial line at a truncated write — and a
|
|
36
|
+
# filter that died on one of those would take the run's log with it just when
|
|
37
|
+
# something is going wrong. `--unbuffered` is what keeps a `tail -f` live rather
|
|
38
|
+
# than arriving in 4 KB bursts.
|
|
39
|
+
#
|
|
40
|
+
# `jq` IS A HARD PREREQUISITE, the same one the guard hooks and the shared
|
|
41
|
+
# library carry; `doctor` checks its version. Without it this exits non-zero on
|
|
42
|
+
# the first byte and the run's log is empty while the raw `.jsonl` beside it is
|
|
43
|
+
# intact — which is the recoverable half of the pair, and the reason the watcher
|
|
44
|
+
# tees rather than only filtering.
|
|
45
|
+
#
|
|
46
|
+
# Usage: <agent CLI> … --output-format stream-json --verbose | autonomous-format-stream.sh
|
|
47
|
+
#
|
|
48
|
+
# Exit map: this process IS `jq` (`exec`), so the status is `jq`'s — 0 on a
|
|
49
|
+
# clean end of stream, non-zero when `jq` itself could not start or the program
|
|
50
|
+
# failed to compile. No caller in the flow switches on it: the watcher reads the
|
|
51
|
+
# agent CLI's status, not this filter's.
|
|
52
|
+
#
|
|
53
|
+
# REPRO — reproduce any line by hand, with no run and no repository:
|
|
54
|
+
#
|
|
55
|
+
# printf '%s\n' \
|
|
56
|
+
# '{"type":"assistant","message":{"content":[{"type":"text","text":"hello"}]}}' \
|
|
57
|
+
# '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Task","input":{"subagent_type":"general-implementer","description":"port a script"}}]}}' \
|
|
58
|
+
# '{"type":"assistant","message":{"content":[{"type":"thinking","thinking":"dropped"}]}}' \
|
|
59
|
+
# 'not json at all' \
|
|
60
|
+
# '{"type":"user","message":{"content":[{"type":"tool_result","content":"dropped"}]}}' \
|
|
61
|
+
# '{"type":"result","subtype":"success","num_turns":3,"total_cost_usd":0.5,"usage":{"input_tokens":10,"output_tokens":20,"cache_creation_input_tokens":30,"cache_read_input_tokens":40}}' \
|
|
62
|
+
# | bash <repo>/<scripts_dir>/autonomous-format-stream.sh
|
|
63
|
+
#
|
|
64
|
+
# -> exactly three lines (the text, the dispatch, the result summary), in that
|
|
65
|
+
# order, with the thinking block, the malformed line and the tool result
|
|
66
|
+
# absent, and exit 0.
|
|
67
|
+
|
|
68
|
+
set -u
|
|
69
|
+
|
|
70
|
+
exec jq -Rr --unbuffered '
|
|
71
|
+
(fromjson? // empty)
|
|
72
|
+
| if .type == "assistant" then
|
|
73
|
+
( .message.content[]? |
|
|
74
|
+
if .type == "text" then
|
|
75
|
+
( .text | gsub("^\\s+|\\s+$"; "") ) as $t
|
|
76
|
+
| if ($t | length) > 0 then "💬 " + ($t | gsub("\n"; " ⏎ ") | .[0:300]) else empty end
|
|
77
|
+
elif .type == "tool_use" then
|
|
78
|
+
"→ " + .name + " "
|
|
79
|
+
+ ( ( (.input.subagent_type // "") + " "
|
|
80
|
+
+ (.input.description // .input.command // .input.file_path // "") )
|
|
81
|
+
| tostring | gsub("\n"; " ") | gsub("^\\s+|\\s+$"; "") | .[0:160] )
|
|
82
|
+
else empty end )
|
|
83
|
+
elif .type == "result" then
|
|
84
|
+
(.usage.input_tokens // 0) as $in
|
|
85
|
+
| (.usage.output_tokens // 0) as $out
|
|
86
|
+
| (.usage.cache_creation_input_tokens // 0) as $cw
|
|
87
|
+
| (.usage.cache_read_input_tokens // 0) as $cr
|
|
88
|
+
| "✅ " + (.subtype // "done")
|
|
89
|
+
+ " turns=" + ((.num_turns // 0) | tostring)
|
|
90
|
+
+ " cost=$" + ((.total_cost_usd // 0) | tostring)
|
|
91
|
+
+ " tokens=" + (($in + $out + $cw + $cr) | tostring)
|
|
92
|
+
+ " (in:" + ($in | tostring) + " out:" + ($out | tostring)
|
|
93
|
+
+ " cache-w:" + ($cw | tostring) + " cache-r:" + ($cr | tostring) + ")"
|
|
94
|
+
else empty end
|
|
95
|
+
'
|