tumwater 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -18,6 +18,7 @@ import { listWideScope, queuedFileAtPosition, promptPreview, queuedListRows, que
|
|
|
18
18
|
import { durationLabel, parseRoleListFlag } from "../cli/cli-args.js";
|
|
19
19
|
import { knownRoleIds, knownRoleIdsCached, loadConfig } from "../config/config.js";
|
|
20
20
|
import { errorMessage } from "../text/text.js";
|
|
21
|
+
import { plural } from "../text/phrases.js";
|
|
21
22
|
import { readPromptImageFiles } from "./inbox-attachments.js";
|
|
22
23
|
import { DIRECTOR_ROLE, unknownRoleMessage } from "../roles/roles.js";
|
|
23
24
|
import { submitRolePromptAndWake } from "../operator/operator-intent.js";
|
|
@@ -39,6 +40,12 @@ function failQueueMiss(listed, position) {
|
|
|
39
40
|
}
|
|
40
41
|
return listed;
|
|
41
42
|
}
|
|
43
|
+
/** Resolve a list-wide position to the single role holding it, failing on ambiguity or miss.
|
|
44
|
+
* The no-`--role` resolution the cancel/edit/top branches each make, shared so the three
|
|
45
|
+
* verbs cannot drift from one another or from sayListedOutcome's wordings. */
|
|
46
|
+
function resolveListedRole(root, position) {
|
|
47
|
+
return failQueueMiss(resolveListedQueue(root, listWideScope(root), position), position).role;
|
|
48
|
+
}
|
|
42
49
|
/** Whether a list-wide outcome is one of the two miss shapes — the guard that narrows the
|
|
43
50
|
* miss fields (`roles`, `queued`) without a cast. */
|
|
44
51
|
function isQueueMiss(listed) {
|
|
@@ -113,6 +120,188 @@ function readPromptImages(paths) {
|
|
|
113
120
|
return fail(errorMessage(err));
|
|
114
121
|
}
|
|
115
122
|
}
|
|
123
|
+
/** `--list` mode — the inspection verb: filter the list-wide rows by `--role a,b` and `--grep`,
|
|
124
|
+
* then print the prose list, the `--json` document, or one of the empty/no-match lines. One
|
|
125
|
+
* helper so `--list`'s rules are found by name. */
|
|
126
|
+
function runPromptList(root, args, json, grep, validIds) {
|
|
127
|
+
// One payload, two shapes: the prose render and the --json output both come from the
|
|
128
|
+
// same array, so the per-role positions --cancel consumes can never disagree with the
|
|
129
|
+
// data a script reads. Full text, verbatim: this is the inspection command that tells
|
|
130
|
+
// you what a queued prompt actually says before you cancel it.
|
|
131
|
+
// `--role a,b` filters the list-wide rows, so section order and each section's own
|
|
132
|
+
// numbering stay what `--cancel <n> --role <id>` takes.
|
|
133
|
+
const listRoles = parseRoleListFlag(args, validIds);
|
|
134
|
+
const allRows = queuedListRows(root, null, validIds);
|
|
135
|
+
const scoped = listRoles === null ? allRows : allRows.filter((row) => listRoles.includes(row.role));
|
|
136
|
+
const prompts = queuedRowsMatching(scoped, grep ?? null);
|
|
137
|
+
if (json) {
|
|
138
|
+
// An empty queue still prints the document, the history --json empty-rows precedent.
|
|
139
|
+
sayJson({ prompts });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (prompts.length === 0 && scoped.length > 0) {
|
|
143
|
+
say(`no queued prompt matches "${grep}"`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (prompts.length === 0) {
|
|
147
|
+
say(listRoles === null
|
|
148
|
+
? "every inbox is empty"
|
|
149
|
+
: listRoles.length === 1
|
|
150
|
+
? `${roleLabel(listRoles[0])}'s inbox is empty`
|
|
151
|
+
: `every listed inbox is empty (${listRoles.join(", ")})`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
say(renderQueuedList(prompts, Date.now()));
|
|
155
|
+
}
|
|
156
|
+
/** `--cancel <n>` mode — remove a queued prompt by position, list-wide or `--role`-scoped. One
|
|
157
|
+
* helper so `--cancel`'s rules are found by name. */
|
|
158
|
+
function runPromptCancel(root, position, role) {
|
|
159
|
+
if (role === null) {
|
|
160
|
+
// No --role: the position addresses what --list shows — its per-role sections, each
|
|
161
|
+
// numbered from 1 (the director first, then the roles in catalog order). Resolve across
|
|
162
|
+
// that scope: one role holding the position cancels there, several are ambiguous (the
|
|
163
|
+
// list itself shows two "N." lines), none is a miss. The output names the role, since
|
|
164
|
+
// the caller scoped nothing.
|
|
165
|
+
sayListedOutcome(cancelListedPrompt(root, listWideScope(root), position), position);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const target = role ?? DIRECTOR_ROLE;
|
|
169
|
+
sayPromptOutcome(position, target, runRolePromptCommand(root, target, position, cancelRolePrompt), false);
|
|
170
|
+
}
|
|
171
|
+
/** Run the CLI's image-input gate for one `prompt` surface. Shared by the submit and
|
|
172
|
+
* --edit forms so the refusal promise holds across every CLI attach surface: an image no
|
|
173
|
+
* configured model can read is refused (fail) before a byte is written, and a role that
|
|
174
|
+
* reroutes a blind read is told (say) where the prompt will run. */
|
|
175
|
+
function runPromptImageGate(root, target) {
|
|
176
|
+
const guidance = imagePromptGuidance(root, target);
|
|
177
|
+
if (guidance.refusal)
|
|
178
|
+
fail(guidance.refusal);
|
|
179
|
+
if (guidance.notice)
|
|
180
|
+
say(guidance.notice);
|
|
181
|
+
}
|
|
182
|
+
/** `--edit <n>` mode — replace a queued prompt's body by position: the attachment gate, then
|
|
183
|
+
* the editor for a bare position or the typed/`--file` text, list-wide or `--role`-scoped. One
|
|
184
|
+
* helper so `--edit`'s rules are found by name. */
|
|
185
|
+
function runPromptEdit(root, position, text, attachPaths, role) {
|
|
186
|
+
// The same broken-config policy and list-wide resolution as cancel, one verb over: the
|
|
187
|
+
// file's new content rides the same queue file (position, enqueue stamp, and a pending
|
|
188
|
+
// --at deferral untouched), and a concurrent dequeue reports gone and exits clean. An
|
|
189
|
+
// `--attach` list is read through the same helper the enqueue form uses and appended to
|
|
190
|
+
// the prompt's images in place.
|
|
191
|
+
const images = attachPaths.length > 0 ? readPromptImages(attachPaths) : undefined;
|
|
192
|
+
// Image-input gate, `--edit`'s twin of the submit branch below (plans/image-input.md
|
|
193
|
+
// "Attachments", part 5/5): editing an attachment into a queued prompt is the same
|
|
194
|
+
// run-time discovery the submit gate prevents, so the refusal promise holds across every
|
|
195
|
+
// CLI attach surface — an image no configured model can read is refused before a byte is
|
|
196
|
+
// written, and a role that reroutes a blind read is told where the prompt will run. The
|
|
197
|
+
// target role is resolved first (the same resolution the verb below makes) so the gate can
|
|
198
|
+
// name it.
|
|
199
|
+
if (images !== undefined && images.length > 0) {
|
|
200
|
+
const targetRole = role ?? resolveListedRole(root, position);
|
|
201
|
+
runPromptImageGate(root, targetRole);
|
|
202
|
+
}
|
|
203
|
+
const typed = text;
|
|
204
|
+
if (typed === null) {
|
|
205
|
+
// Bare `--edit <n>`: open the prompt's body in the editor. The role is resolved first
|
|
206
|
+
// and the write goes by file name, so a dequeue while the editor is open reports gone
|
|
207
|
+
// instead of editing the neighbour that shifted into the position.
|
|
208
|
+
const targetRole = role ?? resolveListedRole(root, position);
|
|
209
|
+
let name;
|
|
210
|
+
let body;
|
|
211
|
+
try {
|
|
212
|
+
name = path.basename(queuedFileAtPosition(root, targetRole, position));
|
|
213
|
+
body = readQueuedPromptBody(root, targetRole, name);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return fail(errorMessage(err));
|
|
217
|
+
}
|
|
218
|
+
if (body === null) {
|
|
219
|
+
sayPromptOutcome(position, targetRole, { status: "gone" }, role === null);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const after = editInEditor(body, "prompt.md");
|
|
223
|
+
// An emptied buffer is an abort, not a blank prompt: the queue would otherwise hold a
|
|
224
|
+
// prompt with no text, which the enqueue forms refuse.
|
|
225
|
+
if (after.trim() === "" && body.trim() !== "")
|
|
226
|
+
fail("the edited prompt is empty; nothing changed (use --cancel <n> to drop it)");
|
|
227
|
+
if (after.trim() === body.trim() && images === undefined) {
|
|
228
|
+
say(`prompt ${position} unchanged`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
sayPromptOutcome(position, targetRole, editQueuedFile(root, targetRole, name, after, images), role === null);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
fail(errorMessage(err));
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (role === null) {
|
|
240
|
+
sayListedOutcome(editListedPrompt(root, listWideScope(root), position, typed, images), position);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const target = role ?? DIRECTOR_ROLE;
|
|
244
|
+
sayPromptOutcome(position, target, runRolePromptCommand(root, target, position, (r, role, position) => editRolePrompt(r, role, position, typed, images)), false);
|
|
245
|
+
}
|
|
246
|
+
/** `--top <n>` mode — move a queued prompt to the head of its queue by position, list-wide or
|
|
247
|
+
* `--role`-scoped. One helper so `--top`'s rules are found by name. */
|
|
248
|
+
function runPromptTop(root, position, role) {
|
|
249
|
+
// The same list-wide resolution the cancel branch uses, one verb on: no --role resolves the
|
|
250
|
+
// position across `--list`'s per-role sections and moves it in the resolved role's queue;
|
|
251
|
+
// with --role it is the role-scoped call. A concurrent dequeue reports gone and exits
|
|
252
|
+
// clean; position 1 of the resolved role is "already first".
|
|
253
|
+
if (role === null) {
|
|
254
|
+
const targetRole = resolveListedRole(root, position);
|
|
255
|
+
let outcome;
|
|
256
|
+
try {
|
|
257
|
+
outcome = movePromptToTop(root, targetRole, position);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return fail(errorMessage(err));
|
|
261
|
+
}
|
|
262
|
+
sayTopOutcome(position, targetRole, outcome, true);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const target = role ?? DIRECTOR_ROLE;
|
|
266
|
+
let outcome;
|
|
267
|
+
try {
|
|
268
|
+
outcome = movePromptToTop(root, target, position);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
return fail(errorMessage(err));
|
|
272
|
+
}
|
|
273
|
+
sayTopOutcome(position, target, outcome, false);
|
|
274
|
+
}
|
|
275
|
+
/** The default (enqueue) mode — the steering-prompt submit form: the `--at` due time, the
|
|
276
|
+
* `--attach` read and image-input gate, then submitRolePromptAndWake and the confirmation.
|
|
277
|
+
* One helper so the submit form's rules are found by name. */
|
|
278
|
+
function runPromptSubmit(root, text, atDelayMs, attachPaths, role) {
|
|
279
|
+
const target = role ?? DIRECTOR_ROLE;
|
|
280
|
+
// `--at <duration>` is a delay, not an epoch: the marker's not-before time is now + the
|
|
281
|
+
// parsed duration, computed at submit so the deferral starts when the operator queued it.
|
|
282
|
+
const dueMs = atDelayMs !== null ? Date.now() + atDelayMs : undefined;
|
|
283
|
+
// `--attach <path>`: read and validate through the shared helper (a bad path fails by name
|
|
284
|
+
// before anything is queued). An attachment the target role cannot read is refused here by
|
|
285
|
+
// name — the same gate submitRolePrompt enforces, surfaced early so the CLI exits non-zero
|
|
286
|
+
// with the fix before anything is written or queued; a role that reroutes a blind read is
|
|
287
|
+
// not refused, but the operator is told where the prompt will actually run.
|
|
288
|
+
const images = attachPaths.length > 0 ? readPromptImages(attachPaths) : undefined;
|
|
289
|
+
if (images && images.length > 0) {
|
|
290
|
+
runPromptImageGate(root, target);
|
|
291
|
+
}
|
|
292
|
+
const wake = submitRolePromptAndWake(root, target, text, images, dueMs);
|
|
293
|
+
const attached = images !== undefined ? ` with ${plural(images.length, "image")}` : "";
|
|
294
|
+
const owner = target === DIRECTOR_ROLE ? "the director's" : `${roleLabel(target)}'s`;
|
|
295
|
+
if (atDelayMs !== null) {
|
|
296
|
+
// A deferred prompt's confirmation names the delivery delay, in the same duration
|
|
297
|
+
// vocabulary --at parsed (durationLabel), so the user can re-type it.
|
|
298
|
+
say(`added to ${owner} inbox — delivers in ${durationLabel(atDelayMs)}${attached}`);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
say(`added to ${owner} inbox${attached}`);
|
|
302
|
+
}
|
|
303
|
+
say(wake);
|
|
304
|
+
}
|
|
116
305
|
/** `tumwater prompt [--role <id>] <text|list|cancel <n>|top <n>>`: submit a steering prompt to the
|
|
117
306
|
* director (default) or one role's queue (`--list` takes `--role <id>[,<id>...]`), list what
|
|
118
307
|
* is queued with per-role position numbering, cancel a queued prompt by position, or move
|
|
@@ -150,176 +339,20 @@ export async function cmdPrompt(root, args) {
|
|
|
150
339
|
}
|
|
151
340
|
const role = parsed.role;
|
|
152
341
|
if (parsed.mode === "list") {
|
|
153
|
-
|
|
154
|
-
// same array, so the per-role positions --cancel consumes can never disagree with the
|
|
155
|
-
// data a script reads. Full text, verbatim: this is the inspection command that tells
|
|
156
|
-
// you what a queued prompt actually says before you cancel it.
|
|
157
|
-
// `--role a,b` filters the list-wide rows, so section order and each section's own
|
|
158
|
-
// numbering stay what `--cancel <n> --role <id>` takes.
|
|
159
|
-
const listRoles = parseRoleListFlag(args, validIds);
|
|
160
|
-
const allRows = queuedListRows(root, null, validIds);
|
|
161
|
-
const scoped = listRoles === null ? allRows : allRows.filter((row) => listRoles.includes(row.role));
|
|
162
|
-
const prompts = queuedRowsMatching(scoped, parsed.grep ?? null);
|
|
163
|
-
if (parsed.json) {
|
|
164
|
-
// An empty queue still prints the document, the history --json empty-rows precedent.
|
|
165
|
-
sayJson({ prompts });
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (prompts.length === 0 && scoped.length > 0) {
|
|
169
|
-
say(`no queued prompt matches "${parsed.grep}"`);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (prompts.length === 0) {
|
|
173
|
-
say(listRoles === null
|
|
174
|
-
? "every inbox is empty"
|
|
175
|
-
: listRoles.length === 1
|
|
176
|
-
? `${roleLabel(listRoles[0])}'s inbox is empty`
|
|
177
|
-
: `every listed inbox is empty (${listRoles.join(", ")})`);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
say(renderQueuedList(prompts, Date.now()));
|
|
342
|
+
runPromptList(root, args, parsed.json, parsed.grep, validIds);
|
|
181
343
|
return;
|
|
182
344
|
}
|
|
183
345
|
if (parsed.mode === "cancel") {
|
|
184
|
-
|
|
185
|
-
// No --role: the position addresses what --list shows — its per-role sections, each
|
|
186
|
-
// numbered from 1 (the director first, then the roles in catalog order). Resolve across
|
|
187
|
-
// that scope: one role holding the position cancels there, several are ambiguous (the
|
|
188
|
-
// list itself shows two "N." lines), none is a miss. The output names the role, since
|
|
189
|
-
// the caller scoped nothing.
|
|
190
|
-
sayListedOutcome(cancelListedPrompt(root, listWideScope(root), parsed.position), parsed.position);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
const target = role ?? DIRECTOR_ROLE;
|
|
194
|
-
sayPromptOutcome(parsed.position, target, runRolePromptCommand(root, target, parsed.position, cancelRolePrompt), false);
|
|
346
|
+
runPromptCancel(root, parsed.position, role);
|
|
195
347
|
return;
|
|
196
348
|
}
|
|
197
349
|
if (parsed.mode === "edit") {
|
|
198
|
-
|
|
199
|
-
// file's new content rides the same queue file (position, enqueue stamp, and a pending
|
|
200
|
-
// --at deferral untouched), and a concurrent dequeue reports gone and exits clean. An
|
|
201
|
-
// `--attach` list is read through the same helper the enqueue form uses and appended to
|
|
202
|
-
// the prompt's images in place.
|
|
203
|
-
const images = parsed.attachPaths.length > 0 ? readPromptImages(parsed.attachPaths) : undefined;
|
|
204
|
-
// Image-input gate, `--edit`'s twin of the submit branch below (plans/image-input.md
|
|
205
|
-
// "Attachments", part 5/5): editing an attachment into a queued prompt is the same
|
|
206
|
-
// run-time discovery the submit gate prevents, so the refusal promise holds across every
|
|
207
|
-
// CLI attach surface — an image no configured model can read is refused before a byte is
|
|
208
|
-
// written, and a role that reroutes a blind read is told where the prompt will run. The
|
|
209
|
-
// target role is resolved first (the same resolution the verb below makes) so the gate can
|
|
210
|
-
// name it.
|
|
211
|
-
if (images !== undefined && images.length > 0) {
|
|
212
|
-
const targetRole = role ?? failQueueMiss(resolveListedQueue(root, listWideScope(root), parsed.position), parsed.position).role;
|
|
213
|
-
const guidance = imagePromptGuidance(root, targetRole);
|
|
214
|
-
if (guidance.refusal)
|
|
215
|
-
return fail(guidance.refusal);
|
|
216
|
-
if (guidance.notice)
|
|
217
|
-
say(guidance.notice);
|
|
218
|
-
}
|
|
219
|
-
const typed = parsed.text;
|
|
220
|
-
if (typed === null) {
|
|
221
|
-
// Bare `--edit <n>`: open the prompt's body in the editor. The role is resolved first
|
|
222
|
-
// and the write goes by file name, so a dequeue while the editor is open reports gone
|
|
223
|
-
// instead of editing the neighbour that shifted into the position.
|
|
224
|
-
let target = role;
|
|
225
|
-
if (target === null) {
|
|
226
|
-
const resolved = resolveListedQueue(root, listWideScope(root), parsed.position);
|
|
227
|
-
target = failQueueMiss(resolved, parsed.position).role;
|
|
228
|
-
}
|
|
229
|
-
const targetRole = target;
|
|
230
|
-
let name;
|
|
231
|
-
let body;
|
|
232
|
-
try {
|
|
233
|
-
name = path.basename(queuedFileAtPosition(root, targetRole, parsed.position));
|
|
234
|
-
body = readQueuedPromptBody(root, targetRole, name);
|
|
235
|
-
}
|
|
236
|
-
catch (err) {
|
|
237
|
-
return fail(errorMessage(err));
|
|
238
|
-
}
|
|
239
|
-
if (body === null) {
|
|
240
|
-
sayPromptOutcome(parsed.position, targetRole, { status: "gone" }, role === null);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
const after = editInEditor(body, "prompt.md");
|
|
244
|
-
// An emptied buffer is an abort, not a blank prompt: the queue would otherwise hold a
|
|
245
|
-
// prompt with no text, which the enqueue forms refuse.
|
|
246
|
-
if (after.trim() === "" && body.trim() !== "")
|
|
247
|
-
fail("the edited prompt is empty; nothing changed (use --cancel <n> to drop it)");
|
|
248
|
-
if (after.trim() === body.trim() && images === undefined) {
|
|
249
|
-
say(`prompt ${parsed.position} unchanged`);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
try {
|
|
253
|
-
sayPromptOutcome(parsed.position, targetRole, editQueuedFile(root, targetRole, name, after, images), role === null);
|
|
254
|
-
}
|
|
255
|
-
catch (err) {
|
|
256
|
-
fail(errorMessage(err));
|
|
257
|
-
}
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
if (role === null) {
|
|
261
|
-
sayListedOutcome(editListedPrompt(root, listWideScope(root), parsed.position, typed, images), parsed.position);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const target = role ?? DIRECTOR_ROLE;
|
|
265
|
-
sayPromptOutcome(parsed.position, target, runRolePromptCommand(root, target, parsed.position, (r, role, position) => editRolePrompt(r, role, position, typed, images)), false);
|
|
350
|
+
runPromptEdit(root, parsed.position, parsed.text, parsed.attachPaths, role);
|
|
266
351
|
return;
|
|
267
352
|
}
|
|
268
353
|
if (parsed.mode === "top") {
|
|
269
|
-
|
|
270
|
-
// position across `--list`'s per-role sections and moves it in the resolved role's queue;
|
|
271
|
-
// with --role it is the role-scoped call. A concurrent dequeue reports gone and exits
|
|
272
|
-
// clean; position 1 of the resolved role is "already first".
|
|
273
|
-
if (role === null) {
|
|
274
|
-
const targetRole = failQueueMiss(resolveListedQueue(root, listWideScope(root), parsed.position), parsed.position).role;
|
|
275
|
-
let outcome;
|
|
276
|
-
try {
|
|
277
|
-
outcome = movePromptToTop(root, targetRole, parsed.position);
|
|
278
|
-
}
|
|
279
|
-
catch (err) {
|
|
280
|
-
return fail(errorMessage(err));
|
|
281
|
-
}
|
|
282
|
-
sayTopOutcome(parsed.position, targetRole, outcome, true);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const target = role ?? DIRECTOR_ROLE;
|
|
286
|
-
let outcome;
|
|
287
|
-
try {
|
|
288
|
-
outcome = movePromptToTop(root, target, parsed.position);
|
|
289
|
-
}
|
|
290
|
-
catch (err) {
|
|
291
|
-
return fail(errorMessage(err));
|
|
292
|
-
}
|
|
293
|
-
sayTopOutcome(parsed.position, target, outcome, false);
|
|
354
|
+
runPromptTop(root, parsed.position, role);
|
|
294
355
|
return;
|
|
295
356
|
}
|
|
296
|
-
|
|
297
|
-
// `--at <duration>` is a delay, not an epoch: the marker's not-before time is now + the
|
|
298
|
-
// parsed duration, computed at submit so the deferral starts when the operator queued it.
|
|
299
|
-
const dueMs = parsed.atDelayMs !== null ? Date.now() + parsed.atDelayMs : undefined;
|
|
300
|
-
// `--attach <path>`: read and validate through the shared helper (a bad path fails by name
|
|
301
|
-
// before anything is queued). An attachment the target role cannot read is refused here by
|
|
302
|
-
// name — the same gate submitRolePrompt enforces, surfaced early so the CLI exits non-zero
|
|
303
|
-
// with the fix before anything is written or queued; a role that reroutes a blind read is
|
|
304
|
-
// not refused, but the operator is told where the prompt will actually run.
|
|
305
|
-
const images = parsed.attachPaths.length > 0 ? readPromptImages(parsed.attachPaths) : undefined;
|
|
306
|
-
if (images && images.length > 0) {
|
|
307
|
-
const guidance = imagePromptGuidance(root, target);
|
|
308
|
-
if (guidance.refusal)
|
|
309
|
-
return fail(guidance.refusal);
|
|
310
|
-
if (guidance.notice)
|
|
311
|
-
say(guidance.notice);
|
|
312
|
-
}
|
|
313
|
-
const wake = submitRolePromptAndWake(root, target, parsed.text, images, dueMs);
|
|
314
|
-
const attached = images !== undefined ? ` with ${images.length} image(s)` : "";
|
|
315
|
-
const owner = target === DIRECTOR_ROLE ? "the director's" : `${roleLabel(target)}'s`;
|
|
316
|
-
if (parsed.atDelayMs !== null) {
|
|
317
|
-
// A deferred prompt's confirmation names the delivery delay, in the same duration
|
|
318
|
-
// vocabulary --at parsed (durationLabel), so the user can re-type it.
|
|
319
|
-
say(`added to ${owner} inbox — delivers in ${durationLabel(parsed.atDelayMs)}${attached}`);
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
say(`added to ${owner} inbox${attached}`);
|
|
323
|
-
}
|
|
324
|
-
say(wake);
|
|
357
|
+
runPromptSubmit(root, parsed.text, parsed.atDelayMs, parsed.attachPaths, role);
|
|
325
358
|
}
|
package/dist/src/init/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { saveConfig } from "../config/config.js";
|
|
4
4
|
import { seedConfig } from "../config/config-example.js";
|
|
5
|
-
import { findOnPath, writeTextAtomic } from "../files/files.js";
|
|
5
|
+
import { ensureDir, findOnPath, writeTextAtomic } from "../files/files.js";
|
|
6
6
|
import { GIT_MISSING_MESSAGE, commitAuthorArgs, committerIdentArgs, git, gitTry } from "../git/git-run.js";
|
|
7
7
|
import { hasCommits, isGitRepo } from "../git/git.js";
|
|
8
8
|
import { BRIEF_TEXT_MAX_CHARS, PROMPT_END, PROMPT_START, briefFile, briefTemplate, readBriefText, readmeTemplate, } from "../brief/brief.js";
|
|
@@ -71,25 +71,21 @@ async function isFreshProject(root, withCommits) {
|
|
|
71
71
|
.filter((line) => line !== "");
|
|
72
72
|
return listed.every(isScaffoldingPath);
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
* matching the README's "seeds a git repo" promise for brand-new projects. An existing repo
|
|
78
|
-
* whose README.md carries no tumwater markers (or any repo without a brief, with `adopt`) is
|
|
79
|
-
* adopted instead: the brief goes in TUMWATER.md and README.md is never touched. `--branch` seeds a
|
|
80
|
-
new repository only and is refused on an existing one, where its value would otherwise be silently
|
|
81
|
-
ignored. `dryRun` runs
|
|
82
|
-
* the same validation and computes the same lists, but writes, inits and commits nothing. */
|
|
83
|
-
export async function initProject(root, briefText, branch, opts = {}) {
|
|
84
|
-
const dryRun = opts.dryRun === true;
|
|
74
|
+
/** The single job this helper owns: resolve the template up front, refusing an unknown id and
|
|
75
|
+
* defaulting to the blank template (INV-Q1: before any side effect). */
|
|
76
|
+
function resolveInitTemplate(id) {
|
|
85
77
|
// Resolve the template before any side effect: an unknown id (possible for direct callers of
|
|
86
78
|
// initProject — the CLI parser already validated its own path) is refused up front, and the
|
|
87
79
|
// template shapes everything below.
|
|
88
|
-
const template =
|
|
89
|
-
if (
|
|
90
|
-
throw new Error(unknownTemplateError(
|
|
80
|
+
const template = id ? getTemplate(id) : null;
|
|
81
|
+
if (id && !template) {
|
|
82
|
+
throw new Error(unknownTemplateError(id));
|
|
91
83
|
}
|
|
92
|
-
|
|
84
|
+
return template ?? getTemplate("blank");
|
|
85
|
+
}
|
|
86
|
+
/** The single job this helper owns: join the template's preamble with the operator's words and
|
|
87
|
+
* enforce the brief cap (INV-Q1: before any side effect). */
|
|
88
|
+
function combineInitBrief(tpl, briefText) {
|
|
93
89
|
// The brief records the template's preamble ahead of the operator's words, so their words
|
|
94
90
|
// remain the tail ("latest instruction wins" reads naturally). The combined text rides into
|
|
95
91
|
// every run's prefill, so it obeys the same cap as a bare prompt — reject before any side
|
|
@@ -100,16 +96,26 @@ export async function initProject(root, briefText, branch, opts = {}) {
|
|
|
100
96
|
if (combinedPrompt.length > BRIEF_TEXT_MAX_CHARS) {
|
|
101
97
|
throw new Error(tooLongMessage(`the brief with the ${tpl.id} template's preamble`, combinedPrompt.length, BRIEF_TEXT_MAX_CHARS, PREFILL_REASON));
|
|
102
98
|
}
|
|
99
|
+
return combinedPrompt;
|
|
100
|
+
}
|
|
101
|
+
/** The single job this helper owns: refuse when `git` is missing from PATH (INV-Q1: before any
|
|
102
|
+
* side effect). */
|
|
103
|
+
function requireGitOnPath() {
|
|
103
104
|
if (!findOnPath("git"))
|
|
104
105
|
throw new Error(GIT_MISSING_MESSAGE);
|
|
106
|
+
}
|
|
107
|
+
/** The single job this helper owns: refuse when `root` sits inside a team workspace (INV-Q1:
|
|
108
|
+
* before any side effect). */
|
|
109
|
+
function refuseTeamWorkspace(root) {
|
|
105
110
|
// A team workspace is not a project: seeding it would commit tumwater.json and a phantom
|
|
106
111
|
// .tumwater on the role's branch. Refused before any side effect, dry run included.
|
|
107
112
|
const owner = owningTeamRoot(root);
|
|
108
113
|
if (owner !== undefined)
|
|
109
114
|
throw new Error(teamWorkspaceMessage(owner));
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
}
|
|
116
|
+
/** The single job this helper owns: refuse a bare init that has no prompt to read anywhere
|
|
117
|
+
* (INV-Q1: before any side effect). */
|
|
118
|
+
function refuseMissingBrief(root, prompt, combinedPrompt) {
|
|
113
119
|
// A project brief that already carries the prompt makes it optional: a fresh clone of an
|
|
114
120
|
// initialized project, or a checkout that lost its now-untracked tumwater.json
|
|
115
121
|
// (plans/portability.md §4a/7), re-seeds with a bare `tumwater init` — the existing brief
|
|
@@ -125,6 +131,10 @@ export async function initProject(root, briefText, branch, opts = {}) {
|
|
|
125
131
|
: "";
|
|
126
132
|
throw new Error(`a brief is required: tumwater init <prompt | --file prompt.md>${readmeHint}`);
|
|
127
133
|
}
|
|
134
|
+
}
|
|
135
|
+
/** The single job this helper owns: answer whether `root` is already a git repo, refusing
|
|
136
|
+
* `--branch` on one (INV-Q1: before any side effect). */
|
|
137
|
+
async function resolveInitWasRepo(root, branch) {
|
|
128
138
|
// `--branch` seeds a NEW repository only: an existing repo's checked-out branch is the
|
|
129
139
|
// team's business (`tumwater start --branch` or baseBranch chooses it at runtime), so a
|
|
130
140
|
// --branch given here would otherwise be silently ignored — the exact failure mode
|
|
@@ -135,6 +145,12 @@ export async function initProject(root, briefText, branch, opts = {}) {
|
|
|
135
145
|
if (branch !== undefined && wasRepo) {
|
|
136
146
|
throw new Error(`--branch only seeds a new repository: this directory is already a git repository and init leaves its checked-out branch alone — drop --branch, or point the team at a branch with \`tumwater start --branch ${branch}\` (or baseBranch in tumwater.json)`);
|
|
137
147
|
}
|
|
148
|
+
return wasRepo;
|
|
149
|
+
}
|
|
150
|
+
/** The single job this helper owns: decide adoption — whether the brief goes to TUMWATER.md and
|
|
151
|
+
* README.md is left untouched — refusing an unmarked TUMWATER.md (INV-Q1: before any side
|
|
152
|
+
* effect). */
|
|
153
|
+
function resolveInitAdoption(root, opts) {
|
|
138
154
|
// The roles read the project's reason to exist back out of the project brief on every
|
|
139
155
|
// run (readBriefText). A README.md without the managed section is the project's own
|
|
140
156
|
// documentation, so it is never rewritten: the repo is adopted instead, with the brief in
|
|
@@ -150,6 +166,11 @@ export async function initProject(root, briefText, branch, opts = {}) {
|
|
|
150
166
|
if (adopted && fs.existsSync(path.join(root, "TUMWATER.md"))) {
|
|
151
167
|
throw new Error(`TUMWATER.md already exists without a brief between the tumwater:prompt markers, so your prompt would be lost — add it to TUMWATER.md between ${PROMPT_START} and ${PROMPT_END} (or delete TUMWATER.md so init creates one), then re-run \`tumwater init\``);
|
|
152
168
|
}
|
|
169
|
+
return { brief, readmeExists, adopted };
|
|
170
|
+
}
|
|
171
|
+
/** The single job this helper owns: refuse a changed prompt when a brief already owns the
|
|
172
|
+
* managed sections (INV-Q1: before any side effect). */
|
|
173
|
+
function refuseChangedBrief(root, brief, prompt, combinedPrompt) {
|
|
153
174
|
// The same loss one step later: a brief that already owns the managed sections is never
|
|
154
175
|
// rewritten (init only creates what is absent), so a DIFFERENT prompt given here would be
|
|
155
176
|
// silently dropped while every run keeps running the old one. Refuse it and name the file
|
|
@@ -159,8 +180,13 @@ export async function initProject(root, briefText, branch, opts = {}) {
|
|
|
159
180
|
if (prompt && brief !== null && combinedPrompt !== readBriefText(root)) {
|
|
160
181
|
throw new Error(`${brief} already carries a different brief between the tumwater:prompt markers, and init never rewrites an existing project brief, so your prompt would be lost — to change the prompt, edit it in ${brief} between ${PROMPT_START} and ${PROMPT_END}; to re-seed with the current one, re-run a bare \`tumwater init\``);
|
|
161
182
|
}
|
|
162
|
-
|
|
163
|
-
|
|
183
|
+
}
|
|
184
|
+
/** The single job this helper owns: resolve and validate the new-repo branch, last, only when a
|
|
185
|
+
* repo will be seeded (INV-Q1: before any side effect). */
|
|
186
|
+
async function resolveInitPreferredBranch(root, branch, wasRepo) {
|
|
187
|
+
// Last, resolve the new-repo branch — only when a repo will be seeded — and validate it with
|
|
188
|
+
// git's own rule before any side effect, dry run included.
|
|
189
|
+
let preferredBranch;
|
|
164
190
|
if (!wasRepo) {
|
|
165
191
|
// Branch precedence: the caller's explicit --branch, else git's own init.defaultBranch
|
|
166
192
|
// preference, else main. `git init -b` does not consult init.defaultBranch, so the
|
|
@@ -179,10 +205,49 @@ export async function initProject(root, briefText, branch, opts = {}) {
|
|
|
179
205
|
: `git config init.defaultBranch ${JSON.stringify(preferred)}`;
|
|
180
206
|
throw new Error(`${namedBy} is not a valid git branch name — pick a name like main, trunk, or release/2.0 (git's rules: no spaces, colons, or ~^?*[ characters; no leading/trailing slash or double slash; not a dotted or .lock-suffixed component)`);
|
|
181
207
|
}
|
|
208
|
+
preferredBranch = preferred;
|
|
209
|
+
}
|
|
210
|
+
return preferredBranch;
|
|
211
|
+
}
|
|
212
|
+
/** Validate an init request and resolve everything it needs, in the existing order, with no
|
|
213
|
+
* write, no `git init`, no `created`/`leftAlone` allocation and no commit — every refusal
|
|
214
|
+
* an over-long brief, `--branch` on an existing repo, an invalid branch name or a team
|
|
215
|
+
* workspace (INV-Q1) throws here, ahead of any side effect, dry run included. The new-repo
|
|
216
|
+
* branch is validated last (only when a repo will be seeded) so the invalid-branch refusal
|
|
217
|
+
* still precedes any write. The body composes the named validation jobs in their invocation
|
|
218
|
+
* order (the template resolve, the brief cap, the git check, the team-workspace refusal, the
|
|
219
|
+
* missing-brief refusal, the `--branch`-on-an-existing-repo refusal, the adoption resolve,
|
|
220
|
+
* the changed-brief refusal and the preferred-branch resolve), so each job is found by name;
|
|
221
|
+
* every refusal still throws before any side effect (INV-Q1). Returns the resolved plan for
|
|
222
|
+
* `seedProject` to act on. */
|
|
223
|
+
async function resolveInitPlan(root, briefText, branch, opts) {
|
|
224
|
+
const dryRun = opts.dryRun === true;
|
|
225
|
+
const tpl = resolveInitTemplate(opts.template);
|
|
226
|
+
const combinedPrompt = combineInitBrief(tpl, briefText);
|
|
227
|
+
requireGitOnPath();
|
|
228
|
+
refuseTeamWorkspace(root);
|
|
229
|
+
const prompt = briefText.trim();
|
|
230
|
+
refuseMissingBrief(root, prompt, combinedPrompt);
|
|
231
|
+
const wasRepo = await resolveInitWasRepo(root, branch);
|
|
232
|
+
const { brief, readmeExists, adopted } = resolveInitAdoption(root, opts);
|
|
233
|
+
refuseChangedBrief(root, brief, prompt, combinedPrompt);
|
|
234
|
+
const preferredBranch = await resolveInitPreferredBranch(root, branch, wasRepo);
|
|
235
|
+
return { dryRun, tpl, combinedPrompt, brief, readmeExists, adopted, wasRepo, preferredBranch };
|
|
236
|
+
}
|
|
237
|
+
/** Seed `root` from a resolved `InitPlan`: git init (a new repo only), every absent project
|
|
238
|
+
* file and starter directory, the config, and the commit. Holds the `created`/`leftAlone`
|
|
239
|
+
* bookkeeping locally; performs no validation and never refuses. Returns everything the
|
|
240
|
+
* composed `InitResult` needs except `adopted`, `dryRun` and `template`, which `initProject`
|
|
241
|
+
* reads off `plan`. */
|
|
242
|
+
async function seedProject(root, plan) {
|
|
243
|
+
const { dryRun, tpl, combinedPrompt, brief, readmeExists, adopted, wasRepo, preferredBranch } = plan;
|
|
244
|
+
let repoInitialized = false;
|
|
245
|
+
let createdBranch;
|
|
246
|
+
if (!wasRepo) {
|
|
182
247
|
if (!dryRun)
|
|
183
|
-
await git(root, "init", "-b",
|
|
248
|
+
await git(root, "init", "-b", preferredBranch);
|
|
184
249
|
repoInitialized = true;
|
|
185
|
-
createdBranch =
|
|
250
|
+
createdBranch = preferredBranch;
|
|
186
251
|
}
|
|
187
252
|
// Decided before any project file is written (init only seeds bootstrap on a fresh project).
|
|
188
253
|
// A repo the `git init` above just created (or a dry run would) has no commits, and init
|
|
@@ -254,7 +319,7 @@ export async function initProject(root, briefText, branch, opts = {}) {
|
|
|
254
319
|
// The template's starter directories: empty ones only, before the first run — the team's
|
|
255
320
|
// own runs write any code. Existing directories are left alone, like existing files above.
|
|
256
321
|
for (const dir of tpl.starterDirs) {
|
|
257
|
-
claim(dir, path.join(root, dir), () =>
|
|
322
|
+
claim(dir, path.join(root, dir), () => ensureDir(path.join(root, dir)));
|
|
258
323
|
}
|
|
259
324
|
// The config stays out of the commit pathspec: `git add -- tumwater.json` fails on a path
|
|
260
325
|
// the just-written .gitignore ignores (plans/portability.md §4a/7). It still heads the
|
|
@@ -274,5 +339,19 @@ export async function initProject(root, briefText, branch, opts = {}) {
|
|
|
274
339
|
committed = true;
|
|
275
340
|
}
|
|
276
341
|
}
|
|
277
|
-
return { created, leftAlone, committed, repoInitialized, branch: createdBranch,
|
|
342
|
+
return { created, leftAlone, committed, repoInitialized, branch: createdBranch, bootstrap };
|
|
343
|
+
}
|
|
344
|
+
/** Initialize a repo for tumwater: README (with prompt + status) when no project brief exists
|
|
345
|
+
* yet, PLANS, BUGS, QUESTIONS, PRINCIPLES, tumwater.json, .gitignore — then commit whatever was
|
|
346
|
+
* created. When the cwd is not a git repository yet, one is seeded first (`git init -b main`),
|
|
347
|
+
* matching the README's "seeds a git repo" promise for brand-new projects. An existing repo
|
|
348
|
+
* whose README.md carries no tumwater markers (or any repo without a brief, with `adopt`) is
|
|
349
|
+
* adopted instead: the brief goes in TUMWATER.md and README.md is never touched. `--branch` seeds a
|
|
350
|
+
new repository only and is refused on an existing one, where its value would otherwise be silently
|
|
351
|
+
ignored. `dryRun` runs
|
|
352
|
+
* the same validation and computes the same lists, but writes, inits and commits nothing. */
|
|
353
|
+
export async function initProject(root, briefText, branch, opts = {}) {
|
|
354
|
+
const plan = await resolveInitPlan(root, briefText, branch, opts);
|
|
355
|
+
const seeded = await seedProject(root, plan);
|
|
356
|
+
return { ...seeded, adopted: plan.adopted, dryRun: plan.dryRun, template: plan.tpl.id };
|
|
278
357
|
}
|
|
@@ -225,7 +225,7 @@ export const RETRIABLE_MERGE_RESULTS = new Set([
|
|
|
225
225
|
* conflict (mergeApprovedChange), so both send the change back for revision the same way. A
|
|
226
226
|
* rejection with no `revisionRound` is a fresh change's first round (1). The director never
|
|
227
227
|
* revises. */
|
|
228
|
-
async function recordRejectedChange(root, role, state, rejectedHead, revisionRound, priorCounts) {
|
|
228
|
+
export async function recordRejectedChange(root, role, state, rejectedHead, revisionRound, priorCounts) {
|
|
229
229
|
if (role === DIRECTOR_ROLE)
|
|
230
230
|
return;
|
|
231
231
|
const nextRound = (revisionRound ?? 0) + 1;
|
|
@@ -7,7 +7,7 @@ import { errorMessage } from "../text/text.js";
|
|
|
7
7
|
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
8
8
|
import { saveRunnerState } from "../runner/runner-state.js";
|
|
9
9
|
import { addPostReview } from "../review/post-merge-review.js";
|
|
10
|
-
import { drainPostReviews } from "./merge-post-review.js";
|
|
10
|
+
import { drainPostReviews } from "./merge-post-review-drain.js";
|
|
11
11
|
import { subscribeEvents } from "../events/events.js";
|
|
12
12
|
import { treeDiffFiles } from "../git/git-diff.js";
|
|
13
13
|
import { isExemptDiff } from "../review/exemptions.js";
|
|
@@ -9,6 +9,7 @@ import { CONFIG_BASENAME, configPath } from "../paths.js";
|
|
|
9
9
|
import { withPrimaryWorktreeLockAsync } from "../concurrency/lock.js";
|
|
10
10
|
import { TUMWATER_SOURCE, warnEventBestEffort } from "../events/events.js";
|
|
11
11
|
import { errorFirstLine, errorMessage, joinWithMore } from "../text/text.js";
|
|
12
|
+
import { topAndHidden } from "../collections.js";
|
|
12
13
|
/** Git plumbing for the merge flow: rebasing a worktree branch onto main, inspecting and
|
|
13
14
|
* finishing a conflicted rebase, and fast-forwarding main — the mechanics, with no merge policy
|
|
14
15
|
* of their own. Merge policy (lock → rebase → verify → ff, conflict resolution, batch ff)
|
|
@@ -56,10 +57,8 @@ export async function mainCommitsTouching(wt, since, mainBranch, files) {
|
|
|
56
57
|
const [sha = "", subject = "", ...rest] = record.split("\x1f");
|
|
57
58
|
return { sha, subject, body: capIntentBody(rest.join("\x1f").trim()) };
|
|
58
59
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
omitted: Math.max(0, parsed.length - MAX_INTENT_COMMITS),
|
|
62
|
-
};
|
|
60
|
+
const { visible: commits, hidden: omitted } = topAndHidden(parsed, MAX_INTENT_COMMITS);
|
|
61
|
+
return { commits, omitted };
|
|
63
62
|
}
|
|
64
63
|
/** Attempt to rebase the worktree branch onto main and classify the outcome WITHOUT
|
|
65
64
|
* cleaning up: "rebased" (including already up to date), "conflict" (the rebase stopped on
|
|
@@ -191,7 +190,8 @@ export function conflictedFileHunks(wt, files) {
|
|
|
191
190
|
}
|
|
192
191
|
const text = readTextOrNull(full) ?? "";
|
|
193
192
|
const all = text.includes("\0") ? [] : conflictHunkRanges(text);
|
|
194
|
-
|
|
193
|
+
const { visible: hunks, hidden: more } = topAndHidden(all, MAX_LISTED_HUNKS);
|
|
194
|
+
return { path: f, hunks, more };
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
/** The reason naming every changed file that still carries a conflict marker, or undefined when
|