@mmerterden/multi-agent-pipeline 16.10.0 → 16.11.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 +37 -0
- package/README.md +110 -1
- package/README.tr.md +111 -1
- package/install/templates/copilot-instructions.md +1 -1
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/analysis/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/design-check/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/help/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/language/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/purge/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/resume-local/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/review/SKILL.md +7 -6
- package/pipeline/commands/multi-agent/setup/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/stack/SKILL.md +12 -13
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
- package/pipeline/lib/extract-conventions.sh +19 -18
- package/pipeline/multi-agent-refs/_account-picker.md +1 -1
- package/pipeline/multi-agent-refs/_dev-context.md +2 -2
- package/pipeline/multi-agent-refs/_input-parser.md +1 -1
- package/pipeline/multi-agent-refs/analysis/evidence.md +4 -4
- package/pipeline/multi-agent-refs/analysis/intake.md +112 -36
- package/pipeline/multi-agent-refs/analysis/locked.md +1 -1
- package/pipeline/multi-agent-refs/analysis/render.md +37 -14
- package/pipeline/multi-agent-refs/analysis/synthesis.md +7 -7
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +1 -1
- package/pipeline/multi-agent-refs/analysis-template.md +6 -6
- package/pipeline/multi-agent-refs/channels/confluence.md +1 -0
- package/pipeline/multi-agent-refs/channels/pr.md +2 -2
- package/pipeline/multi-agent-refs/conventions-defaults.md +13 -13
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +1 -1
- package/pipeline/multi-agent-refs/phases/modes.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +10 -6
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +6 -5
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +4 -4
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +7 -7
- package/pipeline/multi-agent-refs/picker-contract.md +29 -4
- package/pipeline/multi-agent-refs/readiness-review.md +1 -1
- package/pipeline/multi-agent-refs/rules.md +4 -4
- package/pipeline/multi-agent-refs/{frontend-guide.md → web-guide.md} +2 -2
- package/pipeline/schemas/analysis-output.schema.json +1 -1
- package/pipeline/schemas/analysis-spec.schema.json +3 -3
- package/pipeline/schemas/prefs.schema.json +18 -2
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/validate-analysis-doc.mjs +7 -3
- package/pipeline/scripts/validate-analysis.mjs +6 -1
- package/pipeline/scripts/write-state.mjs +29 -11
- package/pipeline/skills/.skills-index.json +24 -2
- package/pipeline/skills/shared/README.md +9 -7
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-purge/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +11 -12
- package/pipeline/skills/skills-index.md +5 -3
|
@@ -34,7 +34,7 @@ Apply platform default + open Section 20 risk row
|
|
|
34
34
|
| iOS | feature-first + clean arch | `Features/<Feature>/Sources/<Feature>/{Common,Data,Domain,Presentation}/` | Mirrors SwiftPM module boundaries; keeps each feature self-contained |
|
|
35
35
|
| Android | feature module + clean arch | `feature/<feature>/src/main/java/<package>/{data,domain,ui}/` | Standard Now-in-Android pattern, aligns with Gradle module isolation |
|
|
36
36
|
| Backend | layered | `src/{api,services,repositories,schemas,models}/<feature>/` | Standard FastAPI / Express layering, predictable for new contributors |
|
|
37
|
-
|
|
|
37
|
+
| Web | feature folder | `src/features/<feature>/{components,hooks,api,types}/` | Standard Next.js / React feature-folder convention |
|
|
38
38
|
|
|
39
39
|
## C2 - Class Naming
|
|
40
40
|
|
|
@@ -45,7 +45,7 @@ Apply platform default + open Section 20 risk row
|
|
|
45
45
|
| iOS | `<Feature>ViewModel` | `PassengerFlightViewModel` |
|
|
46
46
|
| Android | `<Feature>ViewModel` | `PassengerFlightViewModel` |
|
|
47
47
|
| Backend | not applicable | (services replace state holders) |
|
|
48
|
-
|
|
|
48
|
+
| Web | `use<Feature>` hook | `usePassengerFlight` |
|
|
49
49
|
|
|
50
50
|
### C2b viewNaming
|
|
51
51
|
|
|
@@ -54,7 +54,7 @@ Apply platform default + open Section 20 risk row
|
|
|
54
54
|
| iOS | `<Feature>View` | `PassengerFlightView` |
|
|
55
55
|
| Android | `<Feature>Screen` | `PassengerFlightScreen` |
|
|
56
56
|
| Backend | not applicable | |
|
|
57
|
-
|
|
|
57
|
+
| Web | `<Feature>Page` | `PassengerFlightPage` |
|
|
58
58
|
|
|
59
59
|
### C2c navigatorNaming
|
|
60
60
|
|
|
@@ -63,7 +63,7 @@ Apply platform default + open Section 20 risk row
|
|
|
63
63
|
| iOS | `<Feature>Coordinator` | `PassengerFlightCoordinator` | Conforms to `<Domain>Router` protocol if present |
|
|
64
64
|
| Android | `<Feature>Navigator` | `PassengerFlightNavigator` | Compose Navigation destination |
|
|
65
65
|
| Backend | not applicable | | |
|
|
66
|
-
|
|
|
66
|
+
| Web | route file under `app/` | `app/passenger-flight/page.tsx` | Next.js App Router default |
|
|
67
67
|
|
|
68
68
|
### C2d useCaseNaming
|
|
69
69
|
|
|
@@ -72,7 +72,7 @@ Apply platform default + open Section 20 risk row
|
|
|
72
72
|
| iOS | `<Verb><Feature>UseCase` | `FetchPassengerFlightUseCase` |
|
|
73
73
|
| Android | `<Verb><Feature>UseCase` | `FetchPassengerFlightUseCase` |
|
|
74
74
|
| Backend | `<Feature>Service` | `PassengerFlightService` |
|
|
75
|
-
|
|
|
75
|
+
| Web | hook with side effect | `usePassengerFlightQuery` |
|
|
76
76
|
|
|
77
77
|
### C2e repositoryNaming
|
|
78
78
|
|
|
@@ -81,7 +81,7 @@ Apply platform default + open Section 20 risk row
|
|
|
81
81
|
| iOS | `<Feature>Repository` (protocol) + `<Feature>RepositoryImpl` (class) | `PassengerFlightRepository`, `PassengerFlightRepositoryImpl` |
|
|
82
82
|
| Android | `<Feature>Repository` (interface) + `<Feature>RepositoryImpl` (class) | same |
|
|
83
83
|
| Backend | `<Feature>Repository` | `PassengerFlightRepository` |
|
|
84
|
-
|
|
|
84
|
+
| Web | `<feature>Api` | `passengerFlightApi` |
|
|
85
85
|
|
|
86
86
|
### C2f dtoNaming
|
|
87
87
|
|
|
@@ -90,7 +90,7 @@ Apply platform default + open Section 20 risk row
|
|
|
90
90
|
| iOS | `<Feature>RequestDTO`, `<Feature>ResponseDTO` | `PassengerFlightRequestDTO`, `PassengerFlightResponseDTO` |
|
|
91
91
|
| Android | `<Feature>Dto` | `PassengerFlightDto` |
|
|
92
92
|
| Backend | Pydantic `<Feature>In`, `<Feature>Out` | `PassengerFlightIn`, `PassengerFlightOut` |
|
|
93
|
-
|
|
|
93
|
+
| Web | TypeScript `<Feature>Dto` | `PassengerFlightDto` |
|
|
94
94
|
|
|
95
95
|
## C3 - UI State Model
|
|
96
96
|
|
|
@@ -99,7 +99,7 @@ Apply platform default + open Section 20 risk row
|
|
|
99
99
|
| iOS | sealed enum | `enum PassengerFlightUIState { case loading, idle(...), error(...) }` |
|
|
100
100
|
| Android | sealed interface | `sealed interface PassengerFlightUiState { data object Loading; data class Idle(...); data class Error(...) }` |
|
|
101
101
|
| Backend | not applicable | services do not hold UI state |
|
|
102
|
-
|
|
|
102
|
+
| Web | discriminated union | `type PassengerFlightState = { kind: 'loading' } | { kind: 'idle'; ... } | { kind: 'error'; ... }` |
|
|
103
103
|
|
|
104
104
|
## C4 - Test Method Naming
|
|
105
105
|
|
|
@@ -109,7 +109,7 @@ Apply platform default + open Section 20 risk row
|
|
|
109
109
|
| iOS XCTest fallback | `test<Scenario>_<Expected>()` | `testFetch_ValidInput_ReturnsIdle()` |
|
|
110
110
|
| Android JUnit5 | `fun <funName>_<scenario>_<expectedBehavior>()` | `fun fetch_validInput_returnsIdle()` |
|
|
111
111
|
| Backend pytest | `def test_<scenario>_<expected>():` | `def test_fetch_valid_input_returns_idle():` |
|
|
112
|
-
|
|
|
112
|
+
| Web Vitest | `it('<does X> when <condition>', ...)` | `it('returns idle when input is valid', ...)` |
|
|
113
113
|
|
|
114
114
|
## C5 - Accessibility Identifier
|
|
115
115
|
|
|
@@ -118,7 +118,7 @@ Apply platform default + open Section 20 risk row
|
|
|
118
118
|
| iOS | dot notation `<feature>.<element>` | `passengerFlight.continueButton` | XCUITest query syntax friendliness |
|
|
119
119
|
| Android | camelCase testTag | `passengerFlightContinueButton` | Compose testTag convention |
|
|
120
120
|
| Backend | OpenAPI operationId camelCase | `fetchPassengerFlight` | Code generation friendly |
|
|
121
|
-
|
|
|
121
|
+
| Web | kebab data-testid `<feature>-<element>` | `passenger-flight-continue-button` | DOM attribute readability |
|
|
122
122
|
|
|
123
123
|
## C6 - Localization Key
|
|
124
124
|
|
|
@@ -127,7 +127,7 @@ Apply platform default + open Section 20 risk row
|
|
|
127
127
|
| iOS | hierarchical dot PascalCase | `PassengerFlight.ContinueButton` | Matches `Localizable.xcstrings` convention |
|
|
128
128
|
| Android | flat snake_case | `passenger_flight_continue_button` | Matches `strings.xml` convention |
|
|
129
129
|
| Backend | not applicable (server-side messages live in error code tables) | | |
|
|
130
|
-
|
|
|
130
|
+
| Web | hierarchical dot camelCase | `passengerFlight.continueButton` | i18next + ICU MessageFormat default |
|
|
131
131
|
|
|
132
132
|
## C8 - SwiftUI Preview macro (iOS only)
|
|
133
133
|
|
|
@@ -137,7 +137,7 @@ Apply platform default + open Section 20 risk row
|
|
|
137
137
|
| iOS legacy | `PreviewProvider` struct | `struct FooView_Previews: PreviewProvider { static var previews: some View { FooView(viewModel: .preview(.idle)) } }` | Pre-Swift 5.9 fallback |
|
|
138
138
|
| Android | not applicable | (Compose `@Preview` is handled under C3 / C4 conventions, no separate field) | |
|
|
139
139
|
| Backend | not applicable | | |
|
|
140
|
-
|
|
|
140
|
+
| Web | not applicable | (Storybook stories live in `.stories.tsx` files, tracked under C2 conventions) | |
|
|
141
141
|
|
|
142
142
|
Detection: scan up to 10 SwiftUI view files in the candidate set; majority pick wins. Confidence `high` if 5+ matching examples, `medium` if 3-4, `low` if 2, `none` if 0 SwiftUI views found (Pass B then omits Section 13.6 with `(N/A: UIKit-only feature)` note per Locked 29).
|
|
143
143
|
|
|
@@ -148,7 +148,7 @@ Detection: scan up to 10 SwiftUI view files in the candidate set; majority pick
|
|
|
148
148
|
| iOS | Manual configurator | `<Domain>DependencyConfigurator.register<Feature>()` | Avoids framework lock-in; testable |
|
|
149
149
|
| Android | Hilt module | `@Module @InstallIn(SingletonComponent::class) class <Feature>Module` | Standard Android DI |
|
|
150
150
|
| Backend FastAPI | `Depends(get_<feature>_service)` | `service: PassengerFlightService = Depends(get_passenger_flight_service)` | Native FastAPI pattern |
|
|
151
|
-
|
|
|
151
|
+
| Web | hook factory | `usePassengerFlightApi()` returns a memoized client | No framework DI needed |
|
|
152
152
|
|
|
153
153
|
## Risk row template (Section 20)
|
|
154
154
|
|
|
@@ -41,7 +41,7 @@ Nothing enabled is not an error: a repo whose stack was never selected legitimat
|
|
|
41
41
|
no toolkit. Record the no-op with the enabled set that was read, so "none applied" is
|
|
42
42
|
distinguishable from "never looked".
|
|
43
43
|
|
|
44
|
-
**Not enabled is not an error here**, unlike component dispatch: a repo whose stack was never selected legitimately has no toolkit, and halting would make the pipeline unusable there. Record the no-op and continue. (This paragraph used to say "a backend or web repo legitimately has no toolkit" - that stopped being true when the
|
|
44
|
+
**Not enabled is not an error here**, unlike component dispatch: a repo whose stack was never selected legitimately has no toolkit, and halting would make the pipeline unusable there. Record the no-op and continue. (This paragraph used to say "a backend or web repo legitimately has no toolkit" - that stopped being true when the web and backend toolkits shipped, and the sentence outlived the fact by several releases.)
|
|
45
45
|
|
|
46
46
|
Two marketplaces may ship the same toolkit name (a public one and a corporate one). Resolve whichever is enabled and record its **name and version** in the ledger entry, because the routing table and the skill set differ between versions - a finding that cites a skill has to be traceable to the version that defined it.
|
|
47
47
|
|
|
@@ -69,7 +69,7 @@ Bu is icin hangi pipeline? / Which pipeline for this task?
|
|
|
69
69
|
2. Kisa / Short Dev (self-contained, Opus) -> Review -> Test -> Commit -> Report
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
`bugfix` and `chore` recommend Short; `feature`, `refactor` and `component` recommend Full. The recommendation is presented first and passed as `ASK_CHOICE_DEFAULT` on hosts without a native picker, because `ask-choice.sh` picks the first option on a non-TTY and option order is not a contract.
|
|
72
|
+
`bugfix` and `chore` recommend Short; `feature`, `refactor` and `component` recommend Full. The recommendation is presented first and passed as `ASK_CHOICE_DEFAULT` on hosts without a native picker, because `ask-choice.sh` picks the first option on a non-TTY and option order is not a contract. Pass it as the **1-based index** (Full = 1, Short = 2), not as the label: labels render in `outputLanguage`, so a label-valued default matches nothing on a `tr` run.
|
|
73
73
|
|
|
74
74
|
**Who is asked.** `/multi-agent` and `/multi-agent:local`. Both autopilot entries always run Full without asking.
|
|
75
75
|
|
|
@@ -35,7 +35,7 @@ Read preferences: `PREFS_FILE="$HOME/.claude/multi-agent-preferences.json"` (if
|
|
|
35
35
|
OUTPUT_LANG=$(jq -r '.global.outputLanguage // "en"' "$PREFS_FILE" 2>/dev/null || echo en)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
From this point on, everything the user reads renders in `$OUTPUT_LANG`: conversational lines, `AskUserQuestion` `question`/`description`, and external payload bodies (PR/Jira/Confluence). English stays only on `
|
|
38
|
+
From this point on, everything the user reads renders in `$OUTPUT_LANG`: conversational lines, `AskUserQuestion` `question`/`label`/`description`, and external payload bodies (PR/Jira/Confluence). English stays only on `header`, commit messages, branch names, PR title prefixes, identifiers. Full matrix: `rules.md` "Language Application".
|
|
39
39
|
|
|
40
40
|
**Model fallback date gate** (same step, once per run): read `prefs.global.modelFallback`. If `premiumTierUntil` is set and in the past, apply the date-gate trigger from `$HOME/.claude/multi-agent-refs/features/model-fallback.md` - `preferredModel` personas dispatch on `fallbackModel` for this run, with the one-line WARN. Dispatch-error and budget triggers in that contract apply per-dispatch later; nothing else to do here.
|
|
41
41
|
|
|
@@ -270,7 +270,8 @@ Scan `$HOME` (maxdepth 2) for project markers (`.xcodeproj`, `Package.swift`, `b
|
|
|
270
270
|
6. Sort: `develop*` first, then `release/*`, then `main`/`master`. Surface through the
|
|
271
271
|
**native picker** per `picker-contract.md` (`AskUserQuestion` on Claude Code,
|
|
272
272
|
`ask_choice.sh` on Copilot CLI) - `question` + `description` in `outputLanguage`,
|
|
273
|
-
`label`
|
|
273
|
+
`label` = the branch name verbatim (a proper noun, never translated) with the recent
|
|
274
|
+
branch first and marked `(Recommended)`. The ASCII sketch
|
|
274
275
|
below is what the options carry, not a menu to print:
|
|
275
276
|
|
|
276
277
|
```
|
|
@@ -550,11 +551,14 @@ Ask the depth question from `$HOME/.claude/multi-agent-refs/phases/modes.md` "Pi
|
|
|
550
551
|
|
|
551
552
|
When the intake carried an analysis document or a Figma reference, say so **inside** the question: Short skips the only two phases that would turn that document into a task breakdown, and the user should learn that before choosing, not after.
|
|
552
553
|
|
|
553
|
-
**Pass the default explicitly.** `ask-choice.sh` picks the FIRST option on a non-TTY, so relying on option order breaks the first time someone reorders them for readability, on the host where nobody is watching:
|
|
554
|
+
**Pass the default explicitly, as an index.** `ask-choice.sh` picks the FIRST option on a non-TTY, so relying on option order breaks the first time someone reorders them for readability, on the host where nobody is watching. The default must be the 1-based index, never the label: labels follow `outputLanguage` (`rules.md` matrix), so `ASK_CHOICE_DEFAULT="Full"` matches nothing once the options render as `Tam` / `Kisa`, falls through, and silently takes option 1 on a non-TTY - the exact failure this step exists to prevent.
|
|
554
555
|
|
|
555
556
|
```bash
|
|
556
|
-
|
|
557
|
-
|
|
557
|
+
# Full is option 1, Short is option 2 (modes.md "Pipeline depth")
|
|
558
|
+
DEPTH_DEFAULT_INDEX=1; [ "$DEPTH_RECOMMENDATION" = "short" ] && DEPTH_DEFAULT_INDEX=2
|
|
559
|
+
ASK_CHOICE_DEFAULT="$DEPTH_DEFAULT_INDEX" \
|
|
560
|
+
$HOME/.claude/lib/ask-choice.sh "<localized: 'Which pipeline for this task?'>" \
|
|
561
|
+
"<localized: 'Full'>" "<localized: 'Short'>"
|
|
558
562
|
```
|
|
559
563
|
|
|
560
564
|
**Persist.** Short sets `state.onlyDevelop = true`; Full leaves it `false`. The key is unchanged - only who sets it changed - so every downstream reader keeps working. Short also flips the Phase 1 and Phase 2 tiles to `skipped` (tracker-contract.md, "Late skip"); pre-marking is forbidden.
|
|
@@ -583,7 +587,7 @@ Log: `Phase 0 Step 7.6: test baseline = {green|red|unknown} ({N} pre-existing fa
|
|
|
583
587
|
3. If `clarityScore >= prefs.clarifyAmbiguous.minScoreToProceed` (default 6) or `stopAndAsk == false` → write `state.clarification` (score + rationale, no questions), proceed to Phase 1 silently.
|
|
584
588
|
4. If `stopAndAsk == true`:
|
|
585
589
|
|
|
586
|
-
- **Interactive runs:** render the questions via `AskUserQuestion` (
|
|
590
|
+
- **Interactive runs:** render the questions via `AskUserQuestion` (per `$HOME/.claude/multi-agent-refs/rules.md` Language Application matrix; `outputLanguage` for `question` + `option.label` + `option.description`, English for `header`). Up to `maxQuestions` questions, each with the recommended option flagged. Persist the user's answers under `state.clarification.userAnswers`.
|
|
587
591
|
- **Autopilot runs:** follow `clarifyAmbiguous.autopilotMode`:
|
|
588
592
|
|
|
589
593
|
| Mode | Behavior |
|
|
@@ -283,15 +283,16 @@ Then ask for the decision with a **native `AskUserQuestion` picker** (never a ty
|
|
|
283
283
|
|
|
284
284
|
- `question`: "Do you approve this plan?" (rendered in `outputLanguage`)
|
|
285
285
|
- `header`: "Plan" (English, <=12 chars)
|
|
286
|
-
- `options
|
|
287
|
-
|
|
288
|
-
-
|
|
286
|
+
- `options` (label + description in `outputLanguage`; the names below are the option
|
|
287
|
+
SEMANTICS, not strings to print):
|
|
288
|
+
- option 1 - "Approve": proceed to development (Phase 3)
|
|
289
|
+
- option 2 - "Cancel": pause the task; resume later with `:resume`
|
|
289
290
|
|
|
290
|
-
The picker's built-in **Other** field is the free-text edit channel: the user types an edit request there (e.g. "also look at the auth service but keep LoginView out of scope") instead of typing a keyword.
|
|
291
|
+
The picker's built-in **Other** field is the free-text edit channel: the user types an edit request there (e.g. "also look at the auth service but keep LoginView out of scope") instead of typing a keyword. Per the `rules.md` matrix `question`, `label` and `description` all follow `outputLanguage`; only `header` stays English. Branch on which option was picked, never on its rendered text.
|
|
291
292
|
|
|
292
293
|
Handle the selection:
|
|
293
294
|
|
|
294
|
-
- **Approve**:
|
|
295
|
+
- **Option 1 (Approve)**:
|
|
295
296
|
- Set `state.phases["2"].planApprovedAt = now()`, bump `planIterations` if not yet set (default 1)
|
|
296
297
|
- Log `🧠 Phase 2: Plan approved (iterations={N}, clarificationRounds={M})`
|
|
297
298
|
- Proceed to Phase 3
|
|
@@ -132,7 +132,7 @@ For each task (respecting dependency order):
|
|
|
132
132
|
release_build_lock ;;
|
|
133
133
|
android) ./gradlew test --tests "{testClass}.{testMethod}" 2>&1 | tail -5 ;;
|
|
134
134
|
backend) pytest "{test_file}::{test_name}" 2>&1 | tail -5 ;;
|
|
135
|
-
|
|
135
|
+
web) npm test -- --testPathPattern="{file}" 2>&1 | tail -5 ;;
|
|
136
136
|
esac
|
|
137
137
|
```
|
|
138
138
|
- Must fail for the RIGHT reason (expected assertion, not compilation error)
|
|
@@ -156,7 +156,7 @@ For each task (respecting dependency order):
|
|
|
156
156
|
- Only if duplication or naming is poor
|
|
157
157
|
- Re-run tests after refactor → still GREEN
|
|
158
158
|
|
|
159
|
-
**Target resolution** (auto-detect once per project, cache in `agent-state.json`; ios resolves scheme + simulator, android resolves module + variant, backend/
|
|
159
|
+
**Target resolution** (auto-detect once per project, cache in `agent-state.json`; ios resolves scheme + simulator, android resolves module + variant, backend/web need none):
|
|
160
160
|
```bash
|
|
161
161
|
case "$STACK" in
|
|
162
162
|
ios) xcodebuild -list -json -project "{projectPath}" 2>/dev/null || xcodebuild -list -json -workspace "{workspacePath}" 2>/dev/null
|
|
@@ -170,7 +170,7 @@ For each task (respecting dependency order):
|
|
|
170
170
|
- **ios, preferred (MCP, multi-agent-toolkit >= 3.0.0)**: `acquire_build_lock` → `mcp__multi-agent-toolkit__ios_xcodebuild({project|workspace, scheme, configuration: "Release", destination: "generic/platform=iOS", derived_data_path: "{worktreePath}/.DerivedData"})` → `release_build_lock`. Returns one line `Build: SUCCESS|FAILURE (E errors, W warnings) [xcresult-<id>]`; on failure drill in via `mcp__multi-agent-toolkit__ios_xcresult({id, mode: "errors"})`, never dump the full log.
|
|
171
171
|
- **ios, fallback (raw)**: same lock pair around `xcodebuild build -scheme "{scheme}" -destination "generic/platform=iOS" -derivedDataPath "{worktreePath}/.DerivedData" 2>&1 | tail -5`.
|
|
172
172
|
- **android**: lock pair around `./gradlew assembleDebug 2>&1 | tail -5` (the Gradle daemon and `build/` outputs contend across parallel worktrees exactly as DerivedData does - the lock applies).
|
|
173
|
-
- **backend /
|
|
173
|
+
- **backend / web**: `python -m compileall .` / `npm run build --if-present 2>&1 | tail -5`; no lock.
|
|
174
174
|
5. If build fails → fix → rebuild (max 3 attempts, track `retryCount` in state).
|
|
175
175
|
6. **Intermediate commit** (after each completed task in the plan):
|
|
176
176
|
```bash
|
|
@@ -222,7 +222,7 @@ release_build_lock() { rm -rf "$BUILD_LOCK"; }
|
|
|
222
222
|
- Phase 4 Step 1 Gate 1 (build gate before review)
|
|
223
223
|
- Phase 4 Step 1 Gate 3 (test gate before review)
|
|
224
224
|
|
|
225
|
-
**Android**: same lock discipline - the Gradle daemon and `build/` outputs contend across parallel worktrees. **Backend/
|
|
225
|
+
**Android**: same lock discipline - the Gradle daemon and `build/` outputs contend across parallel worktrees. **Backend/web** (Python, Node.js): no lock needed - these build/test in parallel without conflicts.
|
|
226
226
|
|
|
227
227
|
---
|
|
228
228
|
|
|
@@ -90,17 +90,17 @@ Branch **deterministically**, no implicit fallback. Read `agent-state.json` and
|
|
|
90
90
|
|
|
91
91
|
**Standard path (also used as fallback when instructionDriven=true but file missing):**
|
|
92
92
|
|
|
93
|
-
1. **Local checkout test prompt** (skip in autopilot): If still in worktree AND Phase 5 was skipped, ask the user. Per `$HOME/.claude/multi-agent-refs/rules.md` Language Application matrix: `
|
|
93
|
+
1. **Local checkout test prompt** (skip in autopilot): If still in worktree AND Phase 5 was skipped, ask the user. Per `$HOME/.claude/multi-agent-refs/rules.md` Language Application matrix: `question`, `label` and `description` follow `outputLanguage`; only `header` stays English (<=12-char chip). Render the picker accordingly:
|
|
94
94
|
- `question` (in `outputLanguage`) - semantically: "Run a quick WIP-checkout test before commit?"
|
|
95
95
|
- `header` (English, ≤12 chars): `"WIP checkout"`
|
|
96
|
-
- `options` (description in `outputLanguage
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
- Default: option 1 (
|
|
100
|
-
- If user picks
|
|
96
|
+
- `options` (label + description both in `outputLanguage`; the semantics below, not these English strings, are what to render):
|
|
97
|
+
- option 1 - semantically "No, continue to commit" marked `(Recommended)`; description: proceed straight to commit + push + PR
|
|
98
|
+
- option 2 - semantically "Yes, WIP checkout"; description: pause so the user can checkout the branch locally and poke around
|
|
99
|
+
- Default: option 1 (by index, not by label - the label is localized)
|
|
100
|
+
- If the user picks option 2 (WIP checkout): follow Phase 5 Steps 2-6. On `"fix:"` → Phase 3. On `"ok"` → resume Phase 6.
|
|
101
101
|
2. Commit confirm prompt (skip in autopilot). Same language matrix:
|
|
102
102
|
- `question` in `outputLanguage` (semantic: "Commit now?")
|
|
103
|
-
- labels
|
|
103
|
+
- labels in `outputLanguage`, semantically "Commit" and "Pause and resume later"; branch on the option index, not on the rendered string
|
|
104
104
|
- No → Pause, user can `resume` later. Yes → continue:
|
|
105
105
|
3. If WIP commit exists from Phase 5 or Step 1: `git reset HEAD~1` to unstage, then re-commit properly
|
|
106
106
|
4. Stage changes: `git add` with specific files (NOT `git add -A` - avoid sensitive files, `.worktrees/`, agent files)
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
`ask_choice(question, options[], { header?, default?, allowFreeText? })`
|
|
11
11
|
|
|
12
12
|
- `question` - rendered in `outputLanguage`.
|
|
13
|
-
- `options[]` - each `{ label (
|
|
14
|
-
- `default` - recommended option
|
|
13
|
+
- `options[]` - each `{ label (outputLanguage), description (outputLanguage) }`. A label naming a branch, an account or another proper noun is passed through verbatim rather than translated.
|
|
14
|
+
- `default` - recommended option, given as a **1-based index**; used by autopilot / non-interactive runs. Labels are localized, so a label-valued default resolves differently on `en` and `tr`.
|
|
15
15
|
- `allowFreeText` - when true, a free-text answer is accepted as an extra channel (e.g. the plan-gate's edit request).
|
|
16
16
|
|
|
17
17
|
Returns the selected option label (or the free-text string when `allowFreeText` and the user typed instead of choosing).
|
|
@@ -33,7 +33,7 @@ This is the surface that makes the native picker show, step by step, what it is
|
|
|
33
33
|
|
|
34
34
|
| Platform | Render |
|
|
35
35
|
|---|---|
|
|
36
|
-
| **Claude Code** | native `AskUserQuestion` (question/description in `outputLanguage`,
|
|
36
|
+
| **Claude Code** | native `AskUserQuestion` (question/label/description in `outputLanguage`, `header` English); free-text via the built-in **Other** field, which the host injects in English and no run can localize |
|
|
37
37
|
| **Copilot CLI** | `pipeline/lib/ask-choice.sh` (numbered menu); a future MCP elicitation tool can replace this once host support is broad |
|
|
38
38
|
|
|
39
39
|
## Universal fallback: `pipeline/lib/ask-choice.sh`
|
|
@@ -45,11 +45,36 @@ choice=$(pipeline/lib/ask-choice.sh "Do you approve this plan?" "Approve" "Cance
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
- Prints the menu + prompt on **stderr**; echoes ONLY the chosen label on **stdout**.
|
|
48
|
-
- `ASK_CHOICE_DEFAULT=<label
|
|
48
|
+
- `ASK_CHOICE_DEFAULT=<1-based-index|label>` selects without prompting (autopilot / CI). The script still resolves either, but pipeline callers **must** pass the index: a label is localized copy and stops matching the moment `outputLanguage` changes.
|
|
49
49
|
- No TTY + no default -> picks the first option and never blocks an automated run.
|
|
50
50
|
|
|
51
51
|
Enforced by `smoke-ask-choice.sh`.
|
|
52
52
|
|
|
53
|
+
## Localized labels: what the caller owns
|
|
54
|
+
|
|
55
|
+
`label` is copy the user reads, so it renders in `outputLanguage` like `question`
|
|
56
|
+
and `description`. Three consequences belong to whoever calls the picker:
|
|
57
|
+
|
|
58
|
+
- **Branch on the choice, not the string.** The returned label is localized, so a
|
|
59
|
+
literal comparison against an English word resolves differently on `en` and `tr`.
|
|
60
|
+
Match on which option was selected and map it back to its canonical English value
|
|
61
|
+
before it reaches `agent-state.json`, a commit message, a branch name or a log line.
|
|
62
|
+
- **Defaults are indices.** Pass `default` and `ASK_CHOICE_DEFAULT` as a 1-based index.
|
|
63
|
+
Both still accept a label, which is exactly the trap: a label-valued default keeps
|
|
64
|
+
working until someone switches `outputLanguage`, then silently matches nothing and
|
|
65
|
+
`ask-choice.sh` falls through to the prompt (or, with no TTY, to option 1).
|
|
66
|
+
- **Proper nouns pass through.** A label naming a branch, an account, a repo or a
|
|
67
|
+
stack id is rendered verbatim, never translated.
|
|
68
|
+
|
|
69
|
+
**Reading a spec file.** Every `label: "..."` written in a phase doc, a ref or a
|
|
70
|
+
command spec is the option's semantics, recorded in English because instruction prose
|
|
71
|
+
is English. It is not the string to print. Render it in `outputLanguage` at call time.
|
|
72
|
+
The same goes for prose that names an option (`If the user picks "Cancel"`): it
|
|
73
|
+
identifies which option, not what the button said.
|
|
74
|
+
|
|
75
|
+
The host injects its own **Other** free-text row in English on every run. Nothing in
|
|
76
|
+
the pipeline can localize it, and no option may depend on its wording.
|
|
77
|
+
|
|
53
78
|
## Autopilot / non-interactive contract
|
|
54
79
|
|
|
55
80
|
In autopilot, `ask_choice` resolves to `default` (or the safe first option) without prompting - identical to how the native gates auto-proceed today. A picker is only surfaced for genuinely ambiguous or destructive decisions, matching the maturity-check model.
|
|
@@ -24,7 +24,7 @@ Maturity is generic; add these pipeline-readiness dimensions by reading the fetc
|
|
|
24
24
|
| Reproduction | Bugs have steps + expected/actual | none (aligns with maturity `no_repro_steps`) |
|
|
25
25
|
| Design reference | UI work links a Figma frame or attaches a screenshot | UI implied but no design source |
|
|
26
26
|
| API contract | backend/integration work names endpoints or a Swagger/OpenAPI ref | data/contract implied, none given |
|
|
27
|
-
| Platform / stack signal | the target stack is inferable (iOS / Android /
|
|
27
|
+
| Platform / stack signal | the target stack is inferable (iOS / Android / web / backend) | ambiguous - Phase 1 could not route |
|
|
28
28
|
| Dependencies | blocking deps/links are called out when they exist | hidden dependency likely |
|
|
29
29
|
|
|
30
30
|
Only emit a dimension as a gap when its trigger applies (do not demand an API contract on a copy-change task). Merge: `gaps = maturity.blockers + maturity.warnings + rubric gaps`.
|
|
@@ -8,8 +8,8 @@ The pipeline has two language axes. Both MUST be applied from the very first tur
|
|
|
8
8
|
|
|
9
9
|
| Axis | Source | Used for |
|
|
10
10
|
|---|---|---|
|
|
11
|
-
| `prefs.global.outputLanguage` (default `"en"`) | Read at Phase 0 Step 0 | All assistant-authored conversational text: status updates, findings, phase headers in chat, summaries, error explanations, audit reports, picker `question` + `description` text, anything the user reads outside the structural UI chrome. |
|
|
12
|
-
| `prefs.global.promptLanguage` (locked to `"en"`) | Hard-coded `"en"` | `AskUserQuestion` `
|
|
11
|
+
| `prefs.global.outputLanguage` (default `"en"`) | Read at Phase 0 Step 0 | All assistant-authored conversational text: status updates, findings, phase headers in chat, summaries, error explanations, audit reports, picker `question` + `label` + `description` text, anything the user reads outside the structural UI chrome. |
|
|
12
|
+
| `prefs.global.promptLanguage` (locked to `"en"`) | Hard-coded `"en"` | `AskUserQuestion` `header` (the <=12-char chip), CLI host error UI, internal contract identifiers. |
|
|
13
13
|
|
|
14
14
|
### Per-field language matrix (canonical)
|
|
15
15
|
|
|
@@ -22,7 +22,7 @@ This is the single source of truth. When a contributor or model is unsure where
|
|
|
22
22
|
| Phase tracker render labels | English (script chrome) |
|
|
23
23
|
| `AskUserQuestion.question` | `outputLanguage` |
|
|
24
24
|
| `AskUserQuestion.options[].description` | `outputLanguage` |
|
|
25
|
-
| `AskUserQuestion.options[].label` |
|
|
25
|
+
| `AskUserQuestion.options[].label` | `outputLanguage` |
|
|
26
26
|
| `AskUserQuestion.header` | English (≤12-char chip) |
|
|
27
27
|
| Confirmation prompt body (e.g. push y/n) | `outputLanguage` |
|
|
28
28
|
| GitHub issue comment body | `outputLanguage` |
|
|
@@ -41,7 +41,7 @@ This is the single source of truth. When a contributor or model is unsure where
|
|
|
41
41
|
|
|
42
42
|
1. At the very start of every run (Phase 0 Step 0, before any status output), call `jq -r '.global.outputLanguage // "en"'` on `$HOME/.claude/multi-agent-preferences.json`. Cache as `OUTPUT_LANG` for the session.
|
|
43
43
|
2. Render every assistant-authored line in `OUTPUT_LANG`. If the user types Turkish but `outputLanguage="en"` is set, follow the pref but suggest `/multi-agent:language tr` once.
|
|
44
|
-
3. `AskUserQuestion` `label` and `
|
|
44
|
+
3. `AskUserQuestion` `question`, `options[].label` and `options[].description` all follow `OUTPUT_LANG`. Only `header` stays English: a <=12-char chip Turkish overflows. Callers branch on which option was picked, never on its literal text, and pass `default` / `ASK_CHOICE_DEFAULT` as a 1-based index. The host's own **Other** row is always English. Caller rules: `picker-contract.md`.
|
|
45
45
|
4. Always English regardless of either axis: commit messages, PR titles, branch names, code identifiers, agent-state.json values, agent-log.md, reviewer/triage system prompts.
|
|
46
46
|
|
|
47
47
|
**Failure mode this prevents.** Entering `/multi-agent`, `/multi-agent:autopilot`, `/multi-agent:local`, etc. and switching the assistant's conversational text or picker question copy to English while `outputLanguage="tr"` is set. The user sees a half-English half-Turkish dialogue, flagged as a pipeline bug, not a stylistic choice.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Web Development Guide
|
|
2
2
|
|
|
3
|
-
When the task involves
|
|
3
|
+
When the task involves web development (React, Next.js, Vue), follow these patterns.
|
|
4
4
|
|
|
5
5
|
### Component Architecture
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"properties": {
|
|
16
16
|
"primary": {
|
|
17
17
|
"type": "string",
|
|
18
|
-
"enum": ["ios", "android", "
|
|
18
|
+
"enum": ["ios", "android", "web", "backend", "mobile", "frontend", "unknown"],
|
|
19
19
|
"description": "Dominant stack inferred from project markers (.xcodeproj, build.gradle, package.json, etc.)."
|
|
20
20
|
},
|
|
21
21
|
"language": {
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"platforms": {
|
|
103
103
|
"type": "array",
|
|
104
104
|
"minItems": 1,
|
|
105
|
-
"items": { "type": "string", "enum": ["ios", "android", "backend", "frontend"] },
|
|
105
|
+
"items": { "type": "string", "enum": ["ios", "android", "web", "backend", "mobile", "frontend"] },
|
|
106
106
|
"description": "Platforms selected in Phase 0 step 3 (multi-select)."
|
|
107
107
|
},
|
|
108
108
|
"repos": {
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"additionalProperties": false,
|
|
113
113
|
"required": ["platform", "name", "canPush"],
|
|
114
114
|
"properties": {
|
|
115
|
-
"platform": { "type": "string", "enum": ["ios", "android", "backend", "frontend"] },
|
|
115
|
+
"platform": { "type": "string", "enum": ["ios", "android", "web", "backend", "mobile", "frontend"] },
|
|
116
116
|
"name": { "type": "string", "minLength": 1 },
|
|
117
117
|
"cloneUrl": { "type": ["string", "null"] },
|
|
118
118
|
"canPush": { "type": "boolean" }
|
|
@@ -417,7 +417,7 @@
|
|
|
417
417
|
"type": "array",
|
|
418
418
|
"items": {
|
|
419
419
|
"type": "string",
|
|
420
|
-
"enum": ["ios", "android", "backend", "frontend"]
|
|
420
|
+
"enum": ["ios", "android", "web", "backend", "mobile", "frontend"]
|
|
421
421
|
}
|
|
422
422
|
},
|
|
423
423
|
"citation": { "type": ["string", "null"] }
|
|
@@ -1704,13 +1704,29 @@
|
|
|
1704
1704
|
"maxItems": 20,
|
|
1705
1705
|
"description": "Extra writable repos to offer in the dev-context picker beyond auto-detected submodules (iOS/Android/Backend)."
|
|
1706
1706
|
},
|
|
1707
|
+
"webRepos": {
|
|
1708
|
+
"type": "array",
|
|
1709
|
+
"items": {
|
|
1710
|
+
"type": "string"
|
|
1711
|
+
},
|
|
1712
|
+
"maxItems": 20,
|
|
1713
|
+
"description": "Web repos for this project, used by /multi-agent:analysis Step 4 (Web is rarely in the iOS/Android submodule tree). Readers fall back to the pre-rename frontendRepos when this is absent."
|
|
1714
|
+
},
|
|
1715
|
+
"webRoots": {
|
|
1716
|
+
"type": "array",
|
|
1717
|
+
"items": {
|
|
1718
|
+
"type": "string"
|
|
1719
|
+
},
|
|
1720
|
+
"maxItems": 20,
|
|
1721
|
+
"description": "Whitelist source roots for the Web repo-evidence scan (overrides the default src/ app/ components/ features/ lib/). Readers fall back to the pre-rename frontendRoots when this is absent."
|
|
1722
|
+
},
|
|
1707
1723
|
"frontendRepos": {
|
|
1708
1724
|
"type": "array",
|
|
1709
1725
|
"items": {
|
|
1710
1726
|
"type": "string"
|
|
1711
1727
|
},
|
|
1712
1728
|
"maxItems": 20,
|
|
1713
|
-
"description": "
|
|
1729
|
+
"description": "Deprecated spelling of webRepos, still read so prefs written before the Web rename keep working. Do not write it for new projects."
|
|
1714
1730
|
},
|
|
1715
1731
|
"frontendRoots": {
|
|
1716
1732
|
"type": "array",
|
|
@@ -1718,7 +1734,7 @@
|
|
|
1718
1734
|
"type": "string"
|
|
1719
1735
|
},
|
|
1720
1736
|
"maxItems": 20,
|
|
1721
|
-
"description": "
|
|
1737
|
+
"description": "Deprecated spelling of webRoots, still read so prefs written before the Web rename keep working. Do not write it for new projects."
|
|
1722
1738
|
},
|
|
1723
1739
|
"appStoreConnect": {
|
|
1724
1740
|
"type": "object",
|
|
@@ -42,9 +42,13 @@
|
|
|
42
42
|
|
|
43
43
|
import { readFileSync } from "node:fs";
|
|
44
44
|
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
45
|
+
// A repo-less run (Locked 35) still splits by channel, derived from the evidence:
|
|
46
|
+
// "mobile" and "web" are its platform values. "none" is the narrower case where the
|
|
47
|
+
// evidence describes no interface at all. All three are real values, not missing ones.
|
|
48
|
+
// "web" is the pre-"web" spelling, still accepted so older documents validate.
|
|
49
|
+
const KNOWN_PLATFORMS = new Set([
|
|
50
|
+
"ios", "android", "web", "backend", "mobile", "frontend", "none",
|
|
51
|
+
]);
|
|
48
52
|
const REQUIRED_FM = ["feature", "platform", "language", "mode", "template_version"];
|
|
49
53
|
|
|
50
54
|
// Never-omitted sections (Locked 2), matched by bilingual title keyword so the
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import { readFileSync } from "node:fs";
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// "frontend" is the pre-v16.11.0 spelling of "web". Kept so a stored Phase 1
|
|
21
|
+
// output, a golden-task fixture or a resumed run written before the rename still
|
|
22
|
+
// validates; new output uses "web". Mirrors analysis-output.schema.json.
|
|
23
|
+
const ALLOWED_STACKS = new Set([
|
|
24
|
+
"ios", "android", "backend", "web", "mobile", "frontend", "unknown",
|
|
25
|
+
]);
|
|
21
26
|
const ALLOWED_SEVERITIES = new Set(["low", "medium", "high"]);
|
|
22
27
|
|
|
23
28
|
function readInput() {
|
|
@@ -100,12 +100,26 @@ async function acquireLock(
|
|
|
100
100
|
/* already gone */
|
|
101
101
|
}
|
|
102
102
|
if (err.code !== "EEXIST") throw err;
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Reclaim by identity, never by path. Between judging a lock stale and
|
|
104
|
+
// deleting it, the holder can release and a THIRD writer can acquire a
|
|
105
|
+
// fresh one - and an unlink by path deletes that live lock, after which
|
|
106
|
+
// two writers hold it and one update is lost. Same failure the PID window
|
|
107
|
+
// above once caused, at a different point in the loop; it survived because
|
|
108
|
+
// it only reproduces under load, where the gap is wide enough to lose.
|
|
109
|
+
//
|
|
110
|
+
// The inode is the identity: a newly acquired lock is a different file
|
|
111
|
+
// even at the same path. Delete only if the file we judged is still the
|
|
112
|
+
// file that is there.
|
|
113
|
+
const stale = lockIdentityIfStale(lockPath, staleMs);
|
|
114
|
+
if (stale) {
|
|
105
115
|
try {
|
|
106
|
-
|
|
116
|
+
const now = statSync(lockPath);
|
|
117
|
+
if (now.ino === stale.ino && now.mtimeMs === stale.mtimeMs) {
|
|
118
|
+
unlinkSync(lockPath);
|
|
119
|
+
}
|
|
120
|
+
// Changed under us: somebody else's live lock. Leave it and retry.
|
|
107
121
|
} catch {
|
|
108
|
-
/*
|
|
122
|
+
/* vanished on its own - fall through and retry */
|
|
109
123
|
}
|
|
110
124
|
continue;
|
|
111
125
|
}
|
|
@@ -128,28 +142,32 @@ async function acquireLock(
|
|
|
128
142
|
* @param {number} staleMs
|
|
129
143
|
* @returns {boolean}
|
|
130
144
|
*/
|
|
131
|
-
function
|
|
145
|
+
function lockIdentityIfStale(lockPath, staleMs) {
|
|
132
146
|
let pid;
|
|
133
147
|
let mtimeMs;
|
|
148
|
+
let ino;
|
|
134
149
|
try {
|
|
135
150
|
pid = parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
|
|
136
|
-
|
|
151
|
+
const st = statSync(lockPath);
|
|
152
|
+
mtimeMs = st.mtimeMs;
|
|
153
|
+
ino = st.ino;
|
|
137
154
|
} catch {
|
|
138
155
|
// Lock vanished between EEXIST and our read - let the retry re-open it.
|
|
139
|
-
return
|
|
156
|
+
return null;
|
|
140
157
|
}
|
|
141
|
-
|
|
158
|
+
const identity = { ino, mtimeMs };
|
|
159
|
+
if (Date.now() - mtimeMs > staleMs) return identity;
|
|
142
160
|
// An unreadable PID is NOT proof of staleness. Treating it as such is what
|
|
143
161
|
// let a writer delete a live lock and lose another writer's update. With the
|
|
144
162
|
// link-based acquire above a lock is never observable without its PID, so
|
|
145
163
|
// this can only be genuine corruption - which the staleMs check reclaims
|
|
146
164
|
// anyway, without racing a writer that is merely mid-flight.
|
|
147
|
-
if (!Number.isInteger(pid) || pid <= 0) return
|
|
165
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
148
166
|
try {
|
|
149
167
|
process.kill(pid, 0); // probe liveness without signalling
|
|
150
|
-
return
|
|
168
|
+
return null; // holder alive
|
|
151
169
|
} catch (err) {
|
|
152
|
-
return err.code === "ESRCH"; // no such process - stale
|
|
170
|
+
return err.code === "ESRCH" ? identity : null; // no such process - stale
|
|
153
171
|
}
|
|
154
172
|
}
|
|
155
173
|
|