@mmerterden/multi-agent-pipeline 20.30.0 → 20.34.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/CHANGELOG.md +123 -0
- package/README.md +47 -1
- package/README.tr.md +61 -1
- package/docs/adr/0018-cross-repo-links-not-a-global-graph.md +43 -0
- package/docs/adr/README.md +2 -1
- package/docs/facts.json +6 -5
- package/docs/features.md +1 -1
- package/install/_dev-only-files.mjs +1 -0
- package/install/_unattended-profile.mjs +61 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +16 -2
- package/install/index.mjs +10 -1
- package/install/templates/claude-hooks.json +23 -1
- package/manifest.json +198 -134
- package/package.json +4 -1
- package/pipeline/agents/code-reviewer.md +9 -0
- package/pipeline/agents/explorer.md +4 -0
- package/pipeline/agents/plan-critic.md +4 -2
- package/pipeline/commands/multi-agent/SKILL.md +17 -136
- package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/channels/SKILL.md +15 -297
- package/pipeline/commands/multi-agent/graph/SKILL.md +5 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/office-on/SKILL.md +8 -7
- package/pipeline/commands/multi-agent/office-status/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
- package/pipeline/commands/multi-agent/resume/SKILL.md +8 -22
- package/pipeline/commands/multi-agent/review/SKILL.md +11 -134
- package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/setup/SKILL.md +19 -224
- package/pipeline/commands/multi-agent/sync/SKILL.md +12 -0
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
- package/pipeline/contract/CHANGELOG.md +46 -0
- package/pipeline/contract/README.md +27 -10
- package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
- package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
- package/pipeline/contract/fixtures/error-unsigned.json +1 -1
- package/pipeline/contract/fixtures/launch-plan.json +1 -1
- package/pipeline/contract/fixtures/runs-awaiting-question.json +6 -1
- package/pipeline/contract/fixtures/runs-empty.json +1 -1
- package/pipeline/contract/fixtures/runs-failed.json +6 -1
- package/pipeline/contract/fixtures/runs-old-schema.json +6 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +5 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +6 -1
- package/pipeline/contract/fixtures/runs-running.json +6 -1
- package/pipeline/contract/manifest.json +153 -19
- package/pipeline/contract/types/index.d.ts +304 -6
- package/pipeline/lib/bg-launch.mjs +106 -0
- package/pipeline/lib/code-digest.mjs +49 -0
- package/pipeline/lib/credential-resolve.mjs +42 -2
- package/pipeline/multi-agent-refs/channels/generator.md +146 -0
- package/pipeline/multi-agent-refs/channels/menu.md +55 -0
- package/pipeline/multi-agent-refs/channels/multi-repo.md +6 -0
- package/pipeline/multi-agent-refs/channels/preview.md +25 -0
- package/pipeline/multi-agent-refs/channels/reference.md +98 -0
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +1 -1
- package/pipeline/multi-agent-refs/features/code-graph.md +27 -0
- package/pipeline/multi-agent-refs/features/office-threat-model.md +24 -6
- package/pipeline/multi-agent-refs/features/plan-critic.md +5 -2
- package/pipeline/multi-agent-refs/features/review-delta.md +4 -1
- package/pipeline/multi-agent-refs/features/scope-check.md +10 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
- package/pipeline/multi-agent-refs/help/en.md +2 -2
- package/pipeline/multi-agent-refs/help/tr.md +2 -2
- package/pipeline/multi-agent-refs/knowledge.md +9 -0
- package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +163 -0
- package/pipeline/multi-agent-refs/phases/modes.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -4
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +9 -8
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +12 -7
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +7 -3
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +19 -10
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/resume/details.md +35 -0
- package/pipeline/multi-agent-refs/review/inputs.md +87 -0
- package/pipeline/multi-agent-refs/review/module-guides.md +25 -0
- package/pipeline/multi-agent-refs/review/valven.md +42 -0
- package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
- package/pipeline/multi-agent-refs/setup/accounts.md +27 -0
- package/pipeline/multi-agent-refs/setup/app-store-connect.md +5 -0
- package/pipeline/multi-agent-refs/setup/firebase.md +5 -0
- package/pipeline/multi-agent-refs/setup/google-play.md +4 -0
- package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +21 -0
- package/pipeline/multi-agent-refs/setup/language.md +46 -0
- package/pipeline/multi-agent-refs/setup/multi-repo-group.md +51 -0
- package/pipeline/multi-agent-refs/setup/summaries.md +63 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +51 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
- package/pipeline/office/NOTICE.md +4 -1
- package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -0
- package/pipeline/office/assets/COFFEE_MACHINE.png +0 -0
- package/pipeline/office/assets/COUNTER_SINK.png +0 -0
- package/pipeline/office/assets/DEVICE_STAND.png +0 -0
- package/pipeline/office/assets/FILING_CABINET.png +0 -0
- package/pipeline/office/assets/FRIDGE.png +0 -0
- package/pipeline/office/assets/MAILBOX.png +0 -0
- package/pipeline/office/assets/MOP_BUCKET.png +0 -0
- package/pipeline/office/assets/PRINTER.png +0 -0
- package/pipeline/office/assets/RECEPTION_DESK.png +0 -0
- package/pipeline/office/assets/SERVER_RACK.png +0 -0
- package/pipeline/office/assets/SERVER_RACK_2.png +0 -0
- package/pipeline/office/assets/TOILET_STALL.png +0 -0
- package/pipeline/office/assets/VENDING.png +0 -0
- package/pipeline/office/assets/WASHBASIN.png +0 -0
- package/pipeline/office/assets/WATER_COOLER.png +0 -0
- package/pipeline/office/index.html +3936 -819
- package/pipeline/office/toolkit.json +3 -0
- package/pipeline/rules/response-shape.md +6 -7
- package/pipeline/schemas/activity.schema.json +188 -4
- package/pipeline/schemas/agent-state.schema.json +511 -115
- package/pipeline/schemas/contract-error.schema.json +12 -1
- package/pipeline/schemas/job-change.schema.json +72 -0
- package/pipeline/schemas/job-request.schema.json +38 -0
- package/pipeline/schemas/launch.json +4 -4
- package/pipeline/schemas/office-credentials.schema.json +71 -0
- package/pipeline/schemas/office-jobs.json +253 -0
- package/pipeline/schemas/office-jobs.schema.json +62 -0
- package/pipeline/schemas/office-memory.schema.json +105 -0
- package/pipeline/schemas/office-settings-request.schema.json +30 -0
- package/pipeline/schemas/office-settings.schema.json +67 -0
- package/pipeline/schemas/phases.json +14 -6
- package/pipeline/schemas/phases.schema.json +86 -19
- package/pipeline/schemas/plan-critique.schema.json +9 -2
- package/pipeline/schemas/planning-output.schema.json +25 -2
- package/pipeline/schemas/prefs.schema.json +24 -1
- package/pipeline/schemas/prs.schema.json +5 -0
- package/pipeline/schemas/prune-logs-request.schema.json +26 -0
- package/pipeline/schemas/prune-logs.schema.json +52 -0
- package/pipeline/schemas/repo-change.schema.json +54 -0
- package/pipeline/schemas/repo-request.schema.json +29 -0
- package/pipeline/schemas/repos.schema.json +49 -1
- package/pipeline/schemas/reviewer-output.schema.json +20 -2
- package/pipeline/schemas/runs-index.schema.json +257 -46
- package/pipeline/schemas/system-status.schema.json +111 -0
- package/pipeline/schemas/token-budget.json +4 -4
- package/pipeline/schemas/tools.schema.json +5 -0
- package/pipeline/schemas/triage-output.schema.json +20 -2
- package/pipeline/schemas/unattended-profile.json +7 -2
- package/pipeline/scripts/_active-run.mjs +2 -1
- package/pipeline/scripts/_graph-analysis.mjs +270 -0
- package/pipeline/scripts/_memory-policy.mjs +165 -0
- package/pipeline/scripts/_secret-scan.mjs +105 -0
- package/pipeline/scripts/_spawn-async.mjs +9 -5
- package/pipeline/scripts/_unified-diff.mjs +2 -2
- package/pipeline/scripts/activity-snapshot.mjs +219 -23
- package/pipeline/scripts/autopilot-runner.mjs +7 -5
- package/pipeline/scripts/capture-flush.sh +12 -2
- package/pipeline/scripts/check-read-size.sh +9 -0
- package/pipeline/scripts/code-graph-rules/android.json +8 -3
- package/pipeline/scripts/contract-server.mjs +575 -28
- package/pipeline/scripts/debt-ledger.mjs +323 -0
- package/pipeline/scripts/file-context.mjs +261 -0
- package/pipeline/scripts/file-context.sh +20 -0
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
- package/pipeline/scripts/graph-links.mjs +230 -0
- package/pipeline/scripts/graph-query.mjs +133 -1
- package/pipeline/scripts/graph-report.mjs +65 -0
- package/pipeline/scripts/job-launch.mjs +477 -0
- package/pipeline/scripts/launch-request.mjs +94 -9
- package/pipeline/scripts/learn-from-transcripts.mjs +12 -48
- package/pipeline/scripts/learnings-ledger.mjs +143 -24
- package/pipeline/scripts/office-build.mjs +148 -11
- package/pipeline/scripts/office-control.mjs +34 -7
- package/pipeline/scripts/office-credentials.mjs +106 -0
- package/pipeline/scripts/office-memory.mjs +200 -0
- package/pipeline/scripts/office-prune.mjs +125 -0
- package/pipeline/scripts/office-settings.mjs +145 -0
- package/pipeline/scripts/phase-tracker.sh +12 -5
- package/pipeline/scripts/plan-critique-gate.mjs +11 -2
- package/pipeline/scripts/prune-logs.sh +53 -2
- package/pipeline/scripts/rework-rulings.mjs +106 -0
- package/pipeline/scripts/runs-index.mjs +62 -2
- package/pipeline/scripts/scope-check-gate.mjs +149 -2
- package/pipeline/scripts/system-status.mjs +196 -0
- package/pipeline/scripts/task-mirror.mjs +150 -0
- package/pipeline/scripts/task-mirror.sh +20 -0
- package/pipeline/scripts/toolkit-tools.mjs +33 -7
- package/pipeline/scripts/transcript-index.mjs +4 -1
- package/pipeline/scripts/triage-memory.mjs +64 -37
- package/pipeline/scripts/validate-planning.mjs +68 -4
- package/pipeline/scripts/validate-reviewer.mjs +20 -0
- package/pipeline/scripts/validate-triage.mjs +107 -2
- package/pipeline/scripts/work-items.mjs +191 -22
- package/pipeline/skills/.skill-manifest.json +9 -9
- package/pipeline/skills/.skills-index.json +1 -1
- package/pipeline/skills/shared/README.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +5 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +3 -4
- package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +21 -0
- package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
- package/pipeline/skills/skills-index.md +1 -1
|
@@ -86,7 +86,7 @@ usage:
|
|
|
86
86
|
phase-tracker.sh model <phase_id> <model_name>
|
|
87
87
|
phase-tracker.sh meta <phase_id> <key> <value>
|
|
88
88
|
phase-tracker.sh now <phase_id> "<text>"
|
|
89
|
-
phase-tracker.sh task <phase_id> <task_id> <pending|in_progress|completed> [--subject S] [--active-form A]
|
|
89
|
+
phase-tracker.sh task <phase_id> <task_id> <pending|in_progress|completed|deleted> [--subject S] [--active-form A]
|
|
90
90
|
phase-tracker.sh cost <phase_id>|total
|
|
91
91
|
phase-tracker.sh tiles
|
|
92
92
|
phase-tracker.sh report
|
|
@@ -1427,11 +1427,11 @@ EOF
|
|
|
1427
1427
|
# client behind it - sees the same list. Quiet like `now`: the host already
|
|
1428
1428
|
# rendered the change, so this only records it.
|
|
1429
1429
|
need_jq
|
|
1430
|
-
[ "$#" -ge 3 ] || { echo "task needs <phase_id> <task_id> <status> [--subject S] [--active-form A]" >&2; exit 64; }
|
|
1430
|
+
[ "$#" -ge 3 ] || { echo "task needs <phase_id> <task_id> <status|deleted> [--subject S] [--active-form A]" >&2; exit 64; }
|
|
1431
1431
|
PID="$1"; TID="$2"; TSTATUS="$3"; shift 3
|
|
1432
1432
|
case "$TSTATUS" in
|
|
1433
|
-
pending|in_progress|completed) ;;
|
|
1434
|
-
*) echo "task: status must be pending, in_progress or
|
|
1433
|
+
pending|in_progress|completed|deleted) ;;
|
|
1434
|
+
*) echo "task: status must be pending, in_progress, completed or deleted" >&2; exit 64 ;;
|
|
1435
1435
|
esac
|
|
1436
1436
|
TSUBJECT=""; HAS_SUBJECT=0; TACTIVE=""; HAS_ACTIVE=0
|
|
1437
1437
|
while [ "$#" -gt 0 ]; do
|
|
@@ -1452,6 +1452,11 @@ EOF
|
|
|
1452
1452
|
fi
|
|
1453
1453
|
EXISTS=$(printf '%s' "$state" | jq -r --arg pid "$PID" --arg tid "$TID" \
|
|
1454
1454
|
'any(.phases[] | select(.id == $pid) | .tasks[]?; .id == $tid)')
|
|
1455
|
+
# A deleted task leaves the list; one never recorded is nothing to delete.
|
|
1456
|
+
if [ "$TSTATUS" = "deleted" ] && [ "$EXISTS" != "true" ]; then
|
|
1457
|
+
release_state_lock
|
|
1458
|
+
exit 0
|
|
1459
|
+
fi
|
|
1455
1460
|
if [ "$EXISTS" != "true" ] && [ "$HAS_SUBJECT" -eq 0 ]; then
|
|
1456
1461
|
release_state_lock
|
|
1457
1462
|
echo "task: a new task needs --subject" >&2
|
|
@@ -1463,7 +1468,9 @@ EOF
|
|
|
1463
1468
|
--arg act "$TACTIVE" --argjson ha "$HAS_ACTIVE" '
|
|
1464
1469
|
.phases |= map(
|
|
1465
1470
|
if .id == $pid then
|
|
1466
|
-
if
|
|
1471
|
+
if $st == "deleted" then
|
|
1472
|
+
.tasks |= map(select(.id != $tid))
|
|
1473
|
+
elif any(.tasks[]?; .id == $tid) then
|
|
1467
1474
|
.tasks |= map(if .id == $tid then
|
|
1468
1475
|
.status = $st
|
|
1469
1476
|
| (if $hs == 1 then .subject = $subj else . end)
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* answer per objection, and a verdict decided by constitution rule id.
|
|
5
5
|
*
|
|
6
6
|
* At the end of Phase 1 the plan-critic persona writes objections to the plan
|
|
7
|
-
* under
|
|
7
|
+
* under six lenses (scope, feasibility, security, a cheaper alternative, an
|
|
8
|
+
* input the spec implies but no test exercises, a decision left to the
|
|
9
|
+
* implementer), and
|
|
8
10
|
* the planner answers each one exactly once: accept and revise the plan, or
|
|
9
11
|
* rebut with evidence. Both halves live in one file,
|
|
10
12
|
* `<worktree>/.pipeline/plan-critique.json` (schemas/plan-critique.schema.json).
|
|
@@ -59,7 +61,14 @@ import { requirementIds } from "./spec-consistency-gate.mjs";
|
|
|
59
61
|
import { resolveOne } from "./verify-citations.mjs";
|
|
60
62
|
|
|
61
63
|
export const GATE = "plan-critique";
|
|
62
|
-
export const LENSES = [
|
|
64
|
+
export const LENSES = [
|
|
65
|
+
"scope",
|
|
66
|
+
"feasibility",
|
|
67
|
+
"security",
|
|
68
|
+
"alternative",
|
|
69
|
+
"unstated-input",
|
|
70
|
+
"underspecified",
|
|
71
|
+
];
|
|
63
72
|
export const ANCHOR_KINDS = ["file", "plan", "analysis", "inference"];
|
|
64
73
|
export const ACTIONS = ["accept", "rebut"];
|
|
65
74
|
const OBJ_ID = /^OBJ-\d{2,3}$/;
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
# PRUNE_GRACE_MIN minutes (default 10) are spared, so pruning while a
|
|
24
24
|
# run is still writing its log dir cannot break resume. An explicit
|
|
25
25
|
# --task=ID bypasses the grace; PRUNE_GRACE_MIN=0 disables it.
|
|
26
|
+
# - resumable runs: a task dir whose agent-state.json status is not
|
|
27
|
+
# finished, abandoned or cancelled is spared however old it is
|
|
28
|
+
# (an unreadable status counts as resumable); --task bypasses this too.
|
|
26
29
|
#
|
|
27
30
|
# Usage:
|
|
28
31
|
# prune-logs.sh # dry-run: every idle task dir
|
|
@@ -31,6 +34,8 @@
|
|
|
31
34
|
# prune-logs.sh --project=my-ios-app # only one project's task dirs
|
|
32
35
|
# prune-logs.sh --task=PROJ-123 # only one task dir
|
|
33
36
|
# prune-logs.sh --older-than=30 --yes
|
|
37
|
+
# prune-logs.sh --only-from=FILE # only the folders FILE lists, one per
|
|
38
|
+
# # line as the dry run prints them
|
|
34
39
|
#
|
|
35
40
|
# Env:
|
|
36
41
|
# PRUNE_LOG_ROOT override the log root (default $HOME/.claude/logs/multi-agent)
|
|
@@ -46,6 +51,7 @@ DELETE=0
|
|
|
46
51
|
OLDER_DAYS=0
|
|
47
52
|
PROJECT=""
|
|
48
53
|
TASK=""
|
|
54
|
+
ONLY_FROM=""
|
|
49
55
|
|
|
50
56
|
usage() {
|
|
51
57
|
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
|
|
@@ -82,6 +88,13 @@ for arg in "$@"; do
|
|
|
82
88
|
exit 2
|
|
83
89
|
fi
|
|
84
90
|
;;
|
|
91
|
+
--only-from=*)
|
|
92
|
+
ONLY_FROM="${arg#*=}"
|
|
93
|
+
if [ ! -f "$ONLY_FROM" ]; then
|
|
94
|
+
echo "prune-logs: --only-from needs a readable list file" >&2
|
|
95
|
+
exit 2
|
|
96
|
+
fi
|
|
97
|
+
;;
|
|
85
98
|
-h | --help)
|
|
86
99
|
usage
|
|
87
100
|
exit 0
|
|
@@ -132,6 +145,31 @@ is_reserved_name() {
|
|
|
132
145
|
return 1
|
|
133
146
|
}
|
|
134
147
|
|
|
148
|
+
# A run that can still be resumed keeps its log however old it is: only a
|
|
149
|
+
# finished, abandoned or cancelled run (or a dir with no recorded status) is
|
|
150
|
+
# pruned; a failed run is one /multi-agent:resume picks up again. --task is
|
|
151
|
+
# an explicit target and bypasses this too.
|
|
152
|
+
run_status() {
|
|
153
|
+
local f="$1/agent-state.json"
|
|
154
|
+
[ -f "$f" ] || return 0
|
|
155
|
+
# A state that cannot be read is treated as resumable ("?"): pruning is
|
|
156
|
+
# the one step here that cannot be undone.
|
|
157
|
+
node -e 'try{const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(j&&typeof j==="object"&&typeof j.status==="string"?j.status:"")}catch{process.stdout.write("?")}' "$f" 2>/dev/null || printf '?'
|
|
158
|
+
}
|
|
159
|
+
is_resumable() {
|
|
160
|
+
case "$(run_status "$1")" in
|
|
161
|
+
"" | complete | completed | done | abandoned | cancelled) return 1 ;;
|
|
162
|
+
esac
|
|
163
|
+
return 0
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# --only-from narrows the match to a confirmed list; every other check still
|
|
167
|
+
# applies, so a listed folder that became active or resumable is spared.
|
|
168
|
+
ONLY_LIST=""
|
|
169
|
+
if [ -n "$ONLY_FROM" ]; then
|
|
170
|
+
ONLY_LIST=$'\n'"$(cat "$ONLY_FROM")"$'\n'
|
|
171
|
+
fi
|
|
172
|
+
|
|
135
173
|
MTIME_ARGS=()
|
|
136
174
|
if [ "$OLDER_DAYS" -gt 0 ]; then
|
|
137
175
|
MTIME_ARGS=(-mtime "+$OLDER_DAYS")
|
|
@@ -139,6 +177,7 @@ fi
|
|
|
139
177
|
|
|
140
178
|
matches=()
|
|
141
179
|
skipped_active=0
|
|
180
|
+
skipped_resumable=0
|
|
142
181
|
while IFS= read -r -d '' d; do
|
|
143
182
|
base="$(basename "$d")"
|
|
144
183
|
if is_reserved_name "$base"; then continue; fi
|
|
@@ -147,6 +186,12 @@ while IFS= read -r -d '' d; do
|
|
|
147
186
|
if [ -n "$TASK" ] && [ "$base" != "$TASK" ]; then continue; fi
|
|
148
187
|
# --project: the immediate parent dir name must equal the project
|
|
149
188
|
if [ -n "$PROJECT" ] && [ "$(basename "$(dirname "$d")")" != "$PROJECT" ]; then continue; fi
|
|
189
|
+
if [ -n "$ONLY_FROM" ]; then
|
|
190
|
+
case "$ONLY_LIST" in
|
|
191
|
+
*$'\n'"${d#"$LOG_ROOT"/}"$'\n'*) ;;
|
|
192
|
+
*) continue ;;
|
|
193
|
+
esac
|
|
194
|
+
fi
|
|
150
195
|
# Active-task grace: spare dirs a run touched in the last GRACE_MIN minutes
|
|
151
196
|
# so a concurrent run keeps a resumable log dir. --task is an explicit,
|
|
152
197
|
# deliberate target and bypasses the grace.
|
|
@@ -155,12 +200,16 @@ while IFS= read -r -d '' d; do
|
|
|
155
200
|
skipped_active=$((skipped_active + 1))
|
|
156
201
|
continue
|
|
157
202
|
fi
|
|
203
|
+
if [ -z "$TASK" ] && is_resumable "$d"; then
|
|
204
|
+
skipped_resumable=$((skipped_resumable + 1))
|
|
205
|
+
continue
|
|
206
|
+
fi
|
|
158
207
|
matches+=("$d")
|
|
159
208
|
done < <(find "$LOG_ROOT" -mindepth 1 -maxdepth 2 -type d ${MTIME_ARGS[@]+"${MTIME_ARGS[@]}"} -print0 2>/dev/null)
|
|
160
209
|
|
|
161
210
|
if [ "${#matches[@]}" -eq 0 ]; then
|
|
162
|
-
if [ "$skipped_active" -gt 0 ]; then
|
|
163
|
-
echo "prune-logs: only active task dirs matched (touched in the last ${GRACE_MIN}m) - spared, nothing to do"
|
|
211
|
+
if [ "$skipped_active" -gt 0 ] || [ "$skipped_resumable" -gt 0 ]; then
|
|
212
|
+
echo "prune-logs: only active or resumable task dirs matched ($skipped_active touched in the last ${GRACE_MIN}m, $skipped_resumable resumable) - spared, nothing to do"
|
|
164
213
|
else
|
|
165
214
|
echo "prune-logs: no task logs matched under $LOG_ROOT - nothing to do"
|
|
166
215
|
fi
|
|
@@ -179,6 +228,7 @@ if [ "$DELETE" -eq 0 ]; then
|
|
|
179
228
|
echo "DRY-RUN - would remove ${#matches[@]} task log dir(s) (frees ~${human}):"
|
|
180
229
|
for d in "${matches[@]}"; do echo " ${d#"$LOG_ROOT"/}"; done
|
|
181
230
|
[ "$skipped_active" -gt 0 ] && echo "Spared $skipped_active active dir(s) touched in the last ${GRACE_MIN}m."
|
|
231
|
+
[ "$skipped_resumable" -gt 0 ] && echo "Spared $skipped_resumable run(s) that can still be resumed."
|
|
182
232
|
echo "Audit trail + metrics corpus are preserved. Re-run with --yes to delete."
|
|
183
233
|
exit 0
|
|
184
234
|
fi
|
|
@@ -199,5 +249,6 @@ for d in "${matches[@]}"; do
|
|
|
199
249
|
done
|
|
200
250
|
spared_note=""
|
|
201
251
|
[ "$skipped_active" -gt 0 ] && spared_note=", spared $skipped_active active"
|
|
252
|
+
[ "$skipped_resumable" -gt 0 ] && spared_note="${spared_note}, spared $skipped_resumable resumable"
|
|
202
253
|
echo "══ prune-logs: removed ${removed}/${#matches[@]} task log dir(s)${spared_note}, freed ~${human} (audit + metrics preserved) ══"
|
|
203
254
|
exit 0
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* rework-rulings.mjs - what happens to each open finding when the rework cap
|
|
4
|
+
* is reached.
|
|
5
|
+
*
|
|
6
|
+
* Phase 2 allows a fixed number of rework rounds. At the cap every accepted
|
|
7
|
+
* finding still open gets one ruling, recorded, never dropped:
|
|
8
|
+
*
|
|
9
|
+
* escalate load-bearing: blocking severity, or a security finding
|
|
10
|
+
* (ruleId SEC-* / CON-SEC-*, a security criteriaSource, or the
|
|
11
|
+
* security-auditor's `security` object). The run stops and the
|
|
12
|
+
* user decides.
|
|
13
|
+
* parked anything else. It leaves the loop as a follow-up with its
|
|
14
|
+
* reason, shown before commit (Phase 4 Step 0b), carried in the
|
|
15
|
+
* PR follow-ups and recorded in the debt list, so the run can finish instead of stopping on a
|
|
16
|
+
* finding that is safe to defer.
|
|
17
|
+
*
|
|
18
|
+
* The run escalates when any ruling is `escalate`; otherwise it continues.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* rework-rulings.mjs --triage <triage-output.json> [--state <agent-state.json>]
|
|
22
|
+
*
|
|
23
|
+
* --state records the result as reviewIterations[-1].rulings.
|
|
24
|
+
*
|
|
25
|
+
* Output: {escalate, parked, rulings[]} as JSON on stdout.
|
|
26
|
+
* Exit: 0 decided (either way) · 2 usage or unreadable triage · 3 state write failed
|
|
27
|
+
*
|
|
28
|
+
* @module pipeline/scripts/rework-rulings
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
32
|
+
import { readRegularJson } from "../lib/regular-file.mjs";
|
|
33
|
+
import { updateState } from "./gate-ledger.mjs";
|
|
34
|
+
|
|
35
|
+
const SECURITY_RULE = /^(CON-)?SEC-/i;
|
|
36
|
+
|
|
37
|
+
/** True when a finding may not leave the loop without the user. */
|
|
38
|
+
export function loadBearing(f) {
|
|
39
|
+
if (f?.severity === "blocking") return true;
|
|
40
|
+
if (typeof f?.ruleId === "string" && SECURITY_RULE.test(f.ruleId)) return true;
|
|
41
|
+
if (f?.security && typeof f.security === "object") return true;
|
|
42
|
+
return typeof f?.criteriaSource === "string" && /security/i.test(f.criteriaSource);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One ruling per accepted finding of a triage output. */
|
|
46
|
+
export function rule(triage) {
|
|
47
|
+
const rulings = (triage?.accepted ?? []).map((f) => {
|
|
48
|
+
const escalate = loadBearing(f);
|
|
49
|
+
return {
|
|
50
|
+
fingerprint: f.fingerprint ?? null,
|
|
51
|
+
file: f.file ?? null,
|
|
52
|
+
line: Number.isInteger(f.line) ? f.line : null,
|
|
53
|
+
severity: f.severity ?? null,
|
|
54
|
+
ruling: escalate ? "escalate" : "parked",
|
|
55
|
+
reason: escalate
|
|
56
|
+
? f.severity === "blocking"
|
|
57
|
+
? "blocking finding open at the rework cap"
|
|
58
|
+
: "security finding open at the rework cap"
|
|
59
|
+
: `rework cap reached; ${f.severity ?? "unrated"} finding parked as a follow-up`,
|
|
60
|
+
issue: f.issue ?? "",
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
escalate: rulings.some((r) => r.ruling === "escalate"),
|
|
65
|
+
parked: rulings.filter((r) => r.ruling === "parked").length,
|
|
66
|
+
rulings,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function flag(argv, name) {
|
|
71
|
+
const i = argv.indexOf(`--${name}`);
|
|
72
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (invokedDirectly(import.meta.url)) {
|
|
76
|
+
const argv = process.argv.slice(2);
|
|
77
|
+
const triagePath = flag(argv, "triage");
|
|
78
|
+
if (!triagePath) {
|
|
79
|
+
console.error("usage: rework-rulings.mjs --triage <path> [--state <path>]");
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
const triage = readRegularJson(triagePath, null);
|
|
83
|
+
if (!triage || !Array.isArray(triage.accepted)) {
|
|
84
|
+
console.error(`rework-rulings: no accepted[] in ${triagePath}`);
|
|
85
|
+
process.exit(2);
|
|
86
|
+
}
|
|
87
|
+
const result = rule(triage);
|
|
88
|
+
const statePath = flag(argv, "state");
|
|
89
|
+
if (statePath) {
|
|
90
|
+
try {
|
|
91
|
+
await updateState(statePath, (s) => {
|
|
92
|
+
const iterations = Array.isArray(s.reviewIterations) ? [...s.reviewIterations] : [];
|
|
93
|
+
if (!iterations.length) iterations.push({ iteration: 1 });
|
|
94
|
+
iterations[iterations.length - 1] = {
|
|
95
|
+
...iterations[iterations.length - 1],
|
|
96
|
+
rulings: result.rulings,
|
|
97
|
+
};
|
|
98
|
+
return { ...s, reviewIterations: iterations };
|
|
99
|
+
});
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error(`rework-rulings: state not written: ${e.message}`);
|
|
102
|
+
process.exitCode = 3;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
106
|
+
}
|
|
@@ -81,12 +81,12 @@ const PHASE_WAITING_FROM = PHASE_CONTRACT.thresholds.waitingFromPhase;
|
|
|
81
81
|
*/
|
|
82
82
|
const LAST_PHASE = PHASE_CONTRACT.phases[PHASE_CONTRACT.phases.length - 1].id;
|
|
83
83
|
|
|
84
|
-
export const CONTRACT_VERSION = "1.
|
|
84
|
+
export const CONTRACT_VERSION = "1.3.0";
|
|
85
85
|
|
|
86
86
|
const STATE_MAX_BYTES = 16 * 1024 * 1024;
|
|
87
87
|
|
|
88
88
|
/** The run fields runs-index.schema.json marks x-sensitivity local. */
|
|
89
|
-
export const LOCAL_RUN_FIELDS = ["dir", "worktreePath", "prUrl", "duplicateOf"];
|
|
89
|
+
export const LOCAL_RUN_FIELDS = ["dir", "worktreePath", "prUrl", "duplicateOf", "channelUrls"];
|
|
90
90
|
|
|
91
91
|
const GROUPS = {
|
|
92
92
|
waiting: "Waiting on you",
|
|
@@ -226,6 +226,57 @@ function runTypeOf(state, kind) {
|
|
|
226
226
|
return "development";
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
const JIRA_KEY = /^[A-Z][A-Z0-9_]{0,30}-[0-9]{1,9}$/;
|
|
230
|
+
const GH_PART = /^[A-Za-z0-9_.-]{1,100}$/;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Where a run's work came from: its Jira key, its GitHub issue as
|
|
234
|
+
* owner/repo#N, or free text. A GitHub issue that got a Jira carries both.
|
|
235
|
+
*/
|
|
236
|
+
export function sourceOf(state) {
|
|
237
|
+
const jira =
|
|
238
|
+
typeof state?.jiraId === "string" && JIRA_KEY.test(state.jiraId) ? state.jiraId : null;
|
|
239
|
+
const g = state?.githubIssue;
|
|
240
|
+
const issue =
|
|
241
|
+
g &&
|
|
242
|
+
GH_PART.test(String(g.owner)) &&
|
|
243
|
+
GH_PART.test(String(g.repo)) &&
|
|
244
|
+
Number.isInteger(g.number) &&
|
|
245
|
+
g.number > 0
|
|
246
|
+
? `${g.owner}/${g.repo}#${g.number}`
|
|
247
|
+
: null;
|
|
248
|
+
if (issue) return { kind: "github", jira, issue };
|
|
249
|
+
if (jira) return { kind: "jira", jira, issue: null };
|
|
250
|
+
if (state?.inputType === "free-text") return { kind: "text", jira: null, issue: null };
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const CHANNELS = ["pr", "jira", "confluence", "wiki", "issue"];
|
|
255
|
+
const CHANNEL_STATES = new Set(["posted", "skipped", "failed"]);
|
|
256
|
+
|
|
257
|
+
/** Where Phase 5 sent the report: each adapter's outcome, in a fixed order. */
|
|
258
|
+
export function channelsOf(state) {
|
|
259
|
+
const r = state?.channelResults;
|
|
260
|
+
if (!r || typeof r !== "object") return { channels: [], urls: {} };
|
|
261
|
+
const channels = [];
|
|
262
|
+
const urls = {};
|
|
263
|
+
for (const c of CHANNELS) {
|
|
264
|
+
const v = r[c];
|
|
265
|
+
if (!v || !CHANNEL_STATES.has(v.status)) continue;
|
|
266
|
+
channels.push({ channel: c, status: v.status });
|
|
267
|
+
if (typeof v.url === "string" && /^https:\/\/\S+$/.test(v.url)) urls[c] = v.url;
|
|
268
|
+
}
|
|
269
|
+
return { channels, urls };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The run's PR number, from the state or its PR address; null when neither names one. */
|
|
273
|
+
export function prNumberOf(state, prUrl) {
|
|
274
|
+
const n = state?.pr?.number;
|
|
275
|
+
if (Number.isInteger(n) && n > 0) return n;
|
|
276
|
+
const m = /\/(?:pull|pull-requests)\/(\d{1,9})(?:[/?#]|$)/.exec(String(prUrl ?? ""));
|
|
277
|
+
return m && Number(m[1]) > 0 ? Number(m[1]) : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
229
280
|
/** The issue summary Phase 0 recorded, else the input the run was launched with. */
|
|
230
281
|
function titleOf(state) {
|
|
231
282
|
return textOrNull(state?.title) ?? textOrNull(state?.launchRequest?.input);
|
|
@@ -388,6 +439,7 @@ export function buildIndex() {
|
|
|
388
439
|
const tracker = readJson(trackerPath);
|
|
389
440
|
const phases = summarisePhases(tracker);
|
|
390
441
|
const prUrl = state?.pr?.url ?? state?.prUrl ?? null;
|
|
442
|
+
const prNumber = prNumberOf(state, prUrl);
|
|
391
443
|
const kind = statePath ? "pipeline" : trackerPath ? "tracker-only" : "empty";
|
|
392
444
|
const currentPhase = Number.isFinite(Number(state?.currentPhase))
|
|
393
445
|
? Number(state.currentPhase)
|
|
@@ -443,7 +495,15 @@ export function buildIndex() {
|
|
|
443
495
|
// exactly what nobody measured.
|
|
444
496
|
unlockedWrites: Number.isInteger(tracker?.unlockedWrites) ? tracker.unlockedWrites : 0,
|
|
445
497
|
runType: runTypeOf(state, kind),
|
|
498
|
+
mode:
|
|
499
|
+
typeof state?.mode === "string" && /^[a-z][a-z-]{0,30}$/.test(state.mode)
|
|
500
|
+
? state.mode
|
|
501
|
+
: null,
|
|
446
502
|
title: titleOf(state),
|
|
503
|
+
source: sourceOf(state),
|
|
504
|
+
prNumber,
|
|
505
|
+
channels: channelsOf(state).channels,
|
|
506
|
+
channelUrls: channelsOf(state).urls,
|
|
447
507
|
progress: runProgress(phases.phases),
|
|
448
508
|
currentTask: currentTaskOf(phases.phases, currentPhase),
|
|
449
509
|
// Whether the session the run was launched under is alive, from
|
|
@@ -10,21 +10,29 @@
|
|
|
10
10
|
* Usage:
|
|
11
11
|
* git diff --name-only <base>...HEAD | node scope-check-gate.mjs --check .pipeline/scope-check.json --diff-files -
|
|
12
12
|
* node scope-check-gate.mjs --check scope-check.json --diff-files files.txt [--advisory]
|
|
13
|
+
* [--patch <unified diff> | --repo <dir> --patch-range <a>...<b>]
|
|
13
14
|
*
|
|
14
|
-
* Output (stdout, JSON): {ok, justified, unjustified[], unlisted[], notDone}
|
|
15
|
+
* Output (stdout, JSON): {ok, justified, unjustified[], unlisted[], notDone, formatOnly[]}
|
|
15
16
|
* unjustified = in the diff, missing from files[] (or with an empty reason)
|
|
16
17
|
* unlisted = in files[], not in the diff (stale entry; reported, never fatal)
|
|
18
|
+
* formatOnly = with --patch, hunks whose removed and added lines differ only
|
|
19
|
+
* in whitespace: a reformat of lines the task did not need to
|
|
20
|
+
* touch. Reported, never fatal. --patch-range reads that diff
|
|
21
|
+
* with git itself, its output format pinned, so a caller needs
|
|
22
|
+
* no file or process substitution for it.
|
|
17
23
|
*
|
|
18
24
|
* Exit codes: 0 every diff file justified (or --advisory), 1 otherwise or on
|
|
19
25
|
* unreadable input, 64 usage.
|
|
20
26
|
*/
|
|
21
27
|
|
|
28
|
+
import { execFileSync } from "node:child_process";
|
|
22
29
|
import { readFileSync } from "node:fs";
|
|
23
30
|
|
|
24
31
|
import { normalizeFile } from "./_fingerprint.mjs";
|
|
32
|
+
import { parseUnifiedDiff } from "./_unified-diff.mjs";
|
|
25
33
|
|
|
26
34
|
const USAGE =
|
|
27
|
-
"usage: scope-check-gate.mjs --check <scope-check.json> --diff-files <path|-> [--advisory]";
|
|
35
|
+
"usage: scope-check-gate.mjs --check <scope-check.json> --diff-files <path|-> [--advisory] [--patch <diff> | --repo <dir> --patch-range <a>...<b>]";
|
|
28
36
|
|
|
29
37
|
function parseArgs(argv) {
|
|
30
38
|
const opts = {};
|
|
@@ -33,15 +41,145 @@ function parseArgs(argv) {
|
|
|
33
41
|
if (a === "--check") opts.check = argv[++i];
|
|
34
42
|
else if (a === "--diff-files") opts.diffFiles = argv[++i];
|
|
35
43
|
else if (a === "--advisory") opts.advisory = true;
|
|
44
|
+
else if (a === "--patch") opts.patch = argv[++i];
|
|
45
|
+
else if (a === "--patch-range") opts.patchRange = argv[++i];
|
|
46
|
+
else if (a === "--repo") opts.repo = argv[++i];
|
|
36
47
|
else return null;
|
|
37
48
|
}
|
|
49
|
+
if (opts.patchRange !== undefined && !/^[A-Za-z0-9_./~^@{}-]+$/.test(opts.patchRange))
|
|
50
|
+
return null;
|
|
51
|
+
if (opts.patchRange?.startsWith("-")) return null;
|
|
38
52
|
return opts.check && opts.diffFiles ? opts : null;
|
|
39
53
|
}
|
|
40
54
|
|
|
55
|
+
/** The diff of a revision range, in the one shape formatOnlyHunks reads. */
|
|
56
|
+
function rangeDiff(repo, range) {
|
|
57
|
+
return execFileSync(
|
|
58
|
+
"git",
|
|
59
|
+
[
|
|
60
|
+
"-C",
|
|
61
|
+
repo || ".",
|
|
62
|
+
"-c",
|
|
63
|
+
"core.quotePath=false",
|
|
64
|
+
"diff",
|
|
65
|
+
"--no-ext-diff",
|
|
66
|
+
"--no-color",
|
|
67
|
+
"--no-textconv",
|
|
68
|
+
"--src-prefix=a/",
|
|
69
|
+
"--dst-prefix=b/",
|
|
70
|
+
range,
|
|
71
|
+
"--",
|
|
72
|
+
],
|
|
73
|
+
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: 30_000 },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
41
77
|
function readText(path) {
|
|
42
78
|
return path === "-" ? readFileSync(0, "utf-8") : readFileSync(path, "utf-8");
|
|
43
79
|
}
|
|
44
80
|
|
|
81
|
+
const INDENT_SENSITIVE = /(\.(py|pyi|ya?ml|coffee|haml|slim|pug)|(^|\/)(GNU)?[Mm]akefile)$/;
|
|
82
|
+
|
|
83
|
+
const LINE_COMMENT = /(\.(py|pyi|ya?ml|sh|bash|zsh|rb|toml|coffee)|(^|\/)(GNU)?[Mm]akefile)$/;
|
|
84
|
+
// Languages where a space next to a bracket, comma, semicolon or colon never
|
|
85
|
+
// carries meaning. YAML, shell, Makefile and CSS are left out on purpose.
|
|
86
|
+
const TIGHT_LANGS =
|
|
87
|
+
/\.(js|jsx|mjs|cjs|ts|tsx|java|go|cs|swift|kt|kts|json|rs|dart|scala|py|pyi|c|h|cc|cpp|hpp|m|mm)$/;
|
|
88
|
+
const LEADING_OPERATOR_LANGS = /\.(kt|kts|swift|go|scala)$/;
|
|
89
|
+
const BREAK_KEEPERS = /\b(return|throw|yield|break|continue)$/;
|
|
90
|
+
const LEADING_OPERATOR = /^[+\-*/%&|^.?]/;
|
|
91
|
+
const TIGHT = /\s*([(){}[\],;:])\s*/g;
|
|
92
|
+
|
|
93
|
+
/** One line with string literals and comments verbatim and every other whitespace run as one space. */
|
|
94
|
+
function foldLine(line, hashComments) {
|
|
95
|
+
const out = [];
|
|
96
|
+
let quote = null;
|
|
97
|
+
let gap = false;
|
|
98
|
+
for (let i = 0; i < line.length; i++) {
|
|
99
|
+
const c = line[i];
|
|
100
|
+
if (quote) {
|
|
101
|
+
out.push(c);
|
|
102
|
+
if (c === "\\") out.push(line[++i] ?? "");
|
|
103
|
+
else if (c === quote) quote = null;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
107
|
+
gap = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (gap && out.length) out.push(" ");
|
|
111
|
+
gap = false;
|
|
112
|
+
if ((c === "/" && line[i + 1] === "/") || (hashComments && c === "#")) {
|
|
113
|
+
out.push(line.slice(i));
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (c === '"' || c === "'" || c === "`") quote = c;
|
|
117
|
+
out.push(c);
|
|
118
|
+
}
|
|
119
|
+
return out.join("");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The layout-free form of some lines. Each line is folded on its own (linear
|
|
124
|
+
* in the hunk), then the lines are joined by a space, except that a break
|
|
125
|
+
* after return, throw, yield, break or continue, and in Kotlin, Swift, Go or
|
|
126
|
+
* Scala a break before a leading operator, stays a break. Space next to
|
|
127
|
+
* brackets, commas, semicolons and colons is dropped only in TIGHT_LANGS, and
|
|
128
|
+
* never in a hunk that holds a C preprocessor line.
|
|
129
|
+
*/
|
|
130
|
+
function outsideStrings(text, file = "") {
|
|
131
|
+
const hashComments = LINE_COMMENT.test(file);
|
|
132
|
+
const lines = text.split("\n").map((l) => foldLine(l, hashComments));
|
|
133
|
+
let joined = "";
|
|
134
|
+
lines.forEach((line, i) => {
|
|
135
|
+
if (i === 0) {
|
|
136
|
+
joined = line;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const keepBreak =
|
|
140
|
+
BREAK_KEEPERS.test(lines[i - 1]) ||
|
|
141
|
+
(LEADING_OPERATOR_LANGS.test(file) && LEADING_OPERATOR.test(line));
|
|
142
|
+
joined += (keepBreak ? "\u23ce" : " ") + line;
|
|
143
|
+
});
|
|
144
|
+
const preprocessor = lines.some((l) => l.startsWith("#")) && !hashComments;
|
|
145
|
+
const tight = TIGHT_LANGS.test(file) && !preprocessor;
|
|
146
|
+
return (tight ? joined.replace(TIGHT, "$1") : joined).trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const indentOf = (line) => /^[ \t]*/.exec(line)[0];
|
|
150
|
+
|
|
151
|
+
/** True when the removed and added lines differ only in layout. */
|
|
152
|
+
function layoutOnly(file, removed, added) {
|
|
153
|
+
if (!removed.length || !added.length) return false;
|
|
154
|
+
if (INDENT_SENSITIVE.test(file)) {
|
|
155
|
+
if (removed.length !== added.length) return false;
|
|
156
|
+
return removed.every(
|
|
157
|
+
(r, i) =>
|
|
158
|
+
indentOf(r.text) === indentOf(added[i].text) &&
|
|
159
|
+
outsideStrings(r.text, file) === outsideStrings(added[i].text, file),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const joined = (lines) => outsideStrings(lines.map((d) => d.text).join("\n"), file);
|
|
163
|
+
return joined(removed) === joined(added);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Hunks of a unified diff whose change is layout only: whitespace outside
|
|
168
|
+
* string literals, and in indentation-sensitive files never a leading indent.
|
|
169
|
+
*/
|
|
170
|
+
export function formatOnlyHunks(patch) {
|
|
171
|
+
const found = [];
|
|
172
|
+
for (const [file, diff] of parseUnifiedDiff(patch)) {
|
|
173
|
+
for (const h of diff.hunks) {
|
|
174
|
+
if (!h.removed.length && !h.added.length) continue;
|
|
175
|
+
if (layoutOnly(file, h.removed, h.added)) {
|
|
176
|
+
found.push({ file: normalizeFile(file), hunk: h.header });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return found;
|
|
181
|
+
}
|
|
182
|
+
|
|
45
183
|
export function evaluate(check, diffFiles) {
|
|
46
184
|
const files = Array.isArray(check?.files) ? check.files : [];
|
|
47
185
|
const reasons = new Map();
|
|
@@ -81,6 +219,15 @@ function main() {
|
|
|
81
219
|
return fail;
|
|
82
220
|
}
|
|
83
221
|
const result = evaluate(check, diffFiles);
|
|
222
|
+
if (opts.patch || opts.patchRange) {
|
|
223
|
+
try {
|
|
224
|
+
const patch = opts.patch ? readText(opts.patch) : rangeDiff(opts.repo, opts.patchRange);
|
|
225
|
+
result.formatOnly = formatOnlyHunks(patch);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
result.formatOnly = [];
|
|
228
|
+
result.patchError = err.message;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
84
231
|
process.stdout.write(JSON.stringify(result) + "\n");
|
|
85
232
|
return result.ok ? 0 : fail;
|
|
86
233
|
}
|