@aperant/framework 0.10.0 → 0.12.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 +236 -1
- package/agents/apt-planner.md +15 -0
- package/agents/apt-verifier.md +15 -10
- package/bin/apt-proof-video.mjs +256 -76
- package/dist/cli/commands/apply-recipe.d.mts +6 -0
- package/dist/cli/commands/apply-recipe.d.mts.map +1 -0
- package/dist/cli/commands/apply-recipe.mjs +98 -0
- package/dist/cli/commands/apply-recipe.mjs.map +1 -0
- package/dist/cli/commands/catch-up.d.mts +9 -0
- package/dist/cli/commands/catch-up.d.mts.map +1 -0
- package/dist/cli/commands/catch-up.mjs +64 -0
- package/dist/cli/commands/catch-up.mjs.map +1 -0
- package/dist/cli/commands/mobile-prepare.d.mts +53 -0
- package/dist/cli/commands/mobile-prepare.d.mts.map +1 -0
- package/dist/cli/commands/mobile-prepare.mjs +139 -0
- package/dist/cli/commands/mobile-prepare.mjs.map +1 -0
- package/dist/cli/commands/pr-review.d.mts +16 -4
- package/dist/cli/commands/pr-review.d.mts.map +1 -1
- package/dist/cli/commands/pr-review.mjs +30 -11
- package/dist/cli/commands/pr-review.mjs.map +1 -1
- package/dist/cli/commands/recipe-diff.d.mts +6 -0
- package/dist/cli/commands/recipe-diff.d.mts.map +1 -0
- package/dist/cli/commands/recipe-diff.mjs +75 -0
- package/dist/cli/commands/recipe-diff.mjs.map +1 -0
- package/dist/cli/commands/roadmap.d.mts.map +1 -1
- package/dist/cli/commands/roadmap.mjs +53 -0
- package/dist/cli/commands/roadmap.mjs.map +1 -1
- package/dist/cli/commands/route.d.mts.map +1 -1
- package/dist/cli/commands/route.mjs +92 -2
- package/dist/cli/commands/route.mjs.map +1 -1
- package/dist/cli/commands/verify-sandbox.d.mts +6 -0
- package/dist/cli/commands/verify-sandbox.d.mts.map +1 -0
- package/dist/cli/commands/verify-sandbox.mjs +200 -0
- package/dist/cli/commands/verify-sandbox.mjs.map +1 -0
- package/dist/cli/config/share-policy.d.mts.map +1 -1
- package/dist/cli/config/share-policy.mjs +1 -0
- package/dist/cli/config/share-policy.mjs.map +1 -1
- package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
- package/dist/cli/config/upgrade-gitignore.mjs +1 -0
- package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
- package/dist/cli/consistency/parse-qa.d.mts.map +1 -1
- package/dist/cli/consistency/parse-qa.mjs +18 -4
- package/dist/cli/consistency/parse-qa.mjs.map +1 -1
- package/dist/cli/consistency/rules/r5-verdict-consistency.d.mts.map +1 -1
- package/dist/cli/consistency/rules/r5-verdict-consistency.mjs +11 -10
- package/dist/cli/consistency/rules/r5-verdict-consistency.mjs.map +1 -1
- package/dist/cli/coordination/catch-up.d.mts +26 -0
- package/dist/cli/coordination/catch-up.d.mts.map +1 -0
- package/dist/cli/coordination/catch-up.mjs +239 -0
- package/dist/cli/coordination/catch-up.mjs.map +1 -0
- package/dist/cli/coordination/last-seen.d.mts +45 -0
- package/dist/cli/coordination/last-seen.d.mts.map +1 -0
- package/dist/cli/coordination/last-seen.mjs +128 -0
- package/dist/cli/coordination/last-seen.mjs.map +1 -0
- package/dist/cli/coordination/store.d.mts +15 -0
- package/dist/cli/coordination/store.d.mts.map +1 -1
- package/dist/cli/coordination/store.mjs +16 -0
- package/dist/cli/coordination/store.mjs.map +1 -1
- package/dist/cli/coverage-check/user-outcomes.d.mts +7 -2
- package/dist/cli/coverage-check/user-outcomes.d.mts.map +1 -1
- package/dist/cli/coverage-check/user-outcomes.mjs +50 -17
- package/dist/cli/coverage-check/user-outcomes.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +30 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/gates/verify-approved.d.mts +0 -48
- package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
- package/dist/cli/gate/gates/verify-approved.mjs +23 -80
- package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
- package/dist/cli/git/default-branch.d.mts +51 -0
- package/dist/cli/git/default-branch.d.mts.map +1 -0
- package/dist/cli/git/default-branch.mjs +234 -0
- package/dist/cli/git/default-branch.mjs.map +1 -0
- package/dist/cli/git/identity.d.mts +3 -5
- package/dist/cli/git/identity.d.mts.map +1 -1
- package/dist/cli/git/identity.mjs +10 -4
- package/dist/cli/git/identity.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +2 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/install/mcp-server-specs.d.mts +6 -5
- package/dist/cli/install/mcp-server-specs.d.mts.map +1 -1
- package/dist/cli/install/mcp-server-specs.mjs +22 -4
- package/dist/cli/install/mcp-server-specs.mjs.map +1 -1
- package/dist/cli/roadmap/backend-adapter.d.mts +43 -0
- package/dist/cli/roadmap/backend-adapter.d.mts.map +1 -0
- package/dist/cli/roadmap/backend-adapter.mjs +222 -0
- package/dist/cli/roadmap/backend-adapter.mjs.map +1 -0
- package/dist/cli/util/runtime-capabilities.d.mts +7 -0
- package/dist/cli/util/runtime-capabilities.d.mts.map +1 -1
- package/dist/cli/util/runtime-capabilities.mjs +65 -0
- package/dist/cli/util/runtime-capabilities.mjs.map +1 -1
- package/dist/cli/verify-proof/manifest-validator.mjs +13 -0
- package/dist/cli/verify-proof/manifest-validator.mjs.map +1 -1
- package/dist/cli/verify-proof/resolver.d.mts +10 -7
- package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
- package/dist/cli/verify-proof/resolver.mjs +28 -7
- package/dist/cli/verify-proof/resolver.mjs.map +1 -1
- package/dist/cli/verify-proof/sandbox/apply.d.mts +64 -0
- package/dist/cli/verify-proof/sandbox/apply.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/apply.mjs +379 -0
- package/dist/cli/verify-proof/sandbox/apply.mjs.map +1 -0
- package/dist/cli/verify-proof/sandbox/gate-predicate.d.mts +63 -0
- package/dist/cli/verify-proof/sandbox/gate-predicate.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/gate-predicate.mjs +115 -0
- package/dist/cli/verify-proof/sandbox/gate-predicate.mjs.map +1 -0
- package/dist/cli/verify-proof/sandbox/pattern-matcher.d.mts +57 -0
- package/dist/cli/verify-proof/sandbox/pattern-matcher.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/pattern-matcher.mjs +124 -0
- package/dist/cli/verify-proof/sandbox/pattern-matcher.mjs.map +1 -0
- package/dist/cli/verify-proof/sandbox/recipe-diff.d.mts +28 -0
- package/dist/cli/verify-proof/sandbox/recipe-diff.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/recipe-diff.mjs +190 -0
- package/dist/cli/verify-proof/sandbox/recipe-diff.mjs.map +1 -0
- package/dist/cli/verify-proof/sandbox/sandbox-config.d.mts +25 -0
- package/dist/cli/verify-proof/sandbox/sandbox-config.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/sandbox-config.mjs +85 -0
- package/dist/cli/verify-proof/sandbox/sandbox-config.mjs.map +1 -0
- package/dist/cli/verify-proof/sandbox/self-test.d.mts +39 -0
- package/dist/cli/verify-proof/sandbox/self-test.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/self-test.mjs +248 -0
- package/dist/cli/verify-proof/sandbox/self-test.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +2 -1
- package/dist/plugin/agents/apt-planner.md +15 -0
- package/dist/plugin/agents/apt-verifier.md +15 -10
- package/dist/plugin/skills/apt/SKILL.md +29 -0
- package/dist/plugin/skills/apt-catch-up/SKILL.md +79 -0
- package/dist/plugin/skills/apt-plan/SKILL.md +23 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +5 -4
- package/dist/plugin/skills/apt-run/SKILL.md +1 -1
- package/dist/plugin/skills/apt-setup/SKILL.md +132 -0
- package/dist/plugin/skills/apt-verify/SKILL.md +20 -15
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +146 -11
- package/dist/proof-report.d.ts.map +1 -1
- package/dist/proof-report.js +4 -1
- package/dist/proof-report.js.map +1 -1
- package/dist/types/config.d.ts +42 -4
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/qa-scoring.d.ts +45 -9
- package/dist/types/qa-scoring.d.ts.map +1 -1
- package/dist/types/qa-scoring.js +34 -13
- package/dist/types/qa-scoring.js.map +1 -1
- package/dist/types/state.d.ts +7 -2
- package/dist/types/state.d.ts.map +1 -1
- package/dist/types/task-record.d.ts +10 -2
- package/dist/types/task-record.d.ts.map +1 -1
- package/drivers/mobile/README.md +40 -0
- package/drivers/mobile/driver.mjs +106 -0
- package/drivers/mobile/manifest.json +49 -0
- package/package.json +1 -1
- package/prompts/inbox-clarification.md +11 -0
- package/prompts/inbox-triage.md +20 -0
- package/prompts/qa_orchestrator_agentic.md +18 -17
- package/prompts/qa_reviewer.md +17 -15
- package/skills/apt/SKILL.md +29 -0
- package/skills/apt-catch-up/SKILL.md +79 -0
- package/skills/apt-plan/SKILL.md +23 -0
- package/skills/apt-pr-review/SKILL.md +5 -4
- package/skills/apt-run/SKILL.md +1 -1
- package/skills/apt-setup/SKILL.md +132 -0
- package/skills/apt-verifier.md +7 -4
- package/skills/apt-verify/SKILL.md +20 -15
- package/skills/apt-verify-proof/SKILL.md +146 -11
- package/src/cli/commands/apply-recipe.mjs +105 -0
- package/src/cli/commands/catch-up.mjs +67 -0
- package/src/cli/commands/mobile-prepare.mjs +151 -0
- package/src/cli/commands/pr-review.mjs +32 -11
- package/src/cli/commands/recipe-diff.mjs +81 -0
- package/src/cli/commands/roadmap.mjs +54 -0
- package/src/cli/commands/route.mjs +92 -1
- package/src/cli/commands/verify-sandbox.mjs +231 -0
- package/src/cli/config/share-policy.mjs +1 -0
- package/src/cli/config/upgrade-gitignore.mjs +1 -0
- package/src/cli/consistency/parse-qa.mjs +20 -4
- package/src/cli/consistency/rules/r5-verdict-consistency.mjs +11 -13
- package/src/cli/coordination/catch-up.mjs +231 -0
- package/src/cli/coordination/last-seen.mjs +131 -0
- package/src/cli/coordination/store.mjs +18 -0
- package/src/cli/coverage-check/user-outcomes.mjs +52 -17
- package/src/cli/dispatch.mjs +29 -0
- package/src/cli/gate/gates/verify-approved.mjs +22 -81
- package/src/cli/git/default-branch.mjs +250 -0
- package/src/cli/git/identity.mjs +9 -3
- package/src/cli/install/legacy-paths.mjs +2 -0
- package/src/cli/install/mcp-server-specs.mjs +24 -4
- package/src/cli/roadmap/backend-adapter.mjs +231 -0
- package/src/cli/util/runtime-capabilities.mjs +67 -0
- package/src/cli/verify-proof/manifest-validator.mjs +15 -0
- package/src/cli/verify-proof/resolver.mjs +27 -7
- package/src/cli/verify-proof/sandbox/apply.mjs +401 -0
- package/src/cli/verify-proof/sandbox/gate-predicate.mjs +126 -0
- package/src/cli/verify-proof/sandbox/pattern-matcher.mjs +127 -0
- package/src/cli/verify-proof/sandbox/recipe-diff.mjs +208 -0
- package/src/cli/verify-proof/sandbox/sandbox-config.mjs +82 -0
- package/src/cli/verify-proof/sandbox/sandbox-schema.json +33 -0
- package/src/cli/verify-proof/sandbox/self-test.mjs +265 -0
- package/templates/config.json +21 -1
- package/templates/proof-verification.md +27 -8
- package/workflows/verify-proof.md +376 -302
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Requires {task_dir}/qa_signoff.json to exist, parse, and carry
|
|
6
6
|
* verdict === "approved" (BUG-013: normalized via parseQaSignoff — case
|
|
7
|
-
* and underscore/hyphen variants collapse to canonical).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* and underscore/hyphen variants collapse to canonical).
|
|
8
|
+
*
|
|
9
|
+
* v0.9.0 (verify-proof-v2-outcome-walker_27-05-26) — the legacy
|
|
10
|
+
* `approved-with-notes` acceptance branch is REMOVED. `verify-approved`
|
|
11
|
+
* only accepts a verdict of exactly `approved`. Tasks whose verify-proof
|
|
12
|
+
* run returned `needs_human` MUST re-run verify-proof with the missing
|
|
13
|
+
* transport installed; tasks whose run returned `rejected` MUST fix the
|
|
14
|
+
* code. There is no middle band.
|
|
11
15
|
*
|
|
12
16
|
* Accepts both `verdict` (newer) and `status` (older) field names — that
|
|
13
17
|
* normalization is delegated to parseQaSignoff.
|
|
@@ -18,39 +22,9 @@ import { join } from 'node:path'
|
|
|
18
22
|
import { parseQaSignoff } from '../../consistency/parse-qa.mjs'
|
|
19
23
|
import { registerGate } from '../registry.mjs'
|
|
20
24
|
|
|
21
|
-
/**
|
|
22
|
-
* Extract dimension scores from the qa_signoff.json shape. We've seen two
|
|
23
|
-
* shapes in the wild:
|
|
24
|
-
* - Array: `dimensions: [{name: 'security', score: 0.9}, ...]`
|
|
25
|
-
* - Object: `dimensions: {security: 0.9, performance: 0.85}` OR
|
|
26
|
-
* `dimensions: {security: {score: 0.9}, ...}`
|
|
27
|
-
* Returns an array of numeric scores, or null when no recognizable shape
|
|
28
|
-
* is present (best-effort accept per brief).
|
|
29
|
-
*/
|
|
30
|
-
function extractDimensionScores(doc) {
|
|
31
|
-
const dims = doc.dimensions ?? doc.qa?.dimensions ?? null
|
|
32
|
-
if (!dims) return null
|
|
33
|
-
if (Array.isArray(dims)) {
|
|
34
|
-
const scores = dims
|
|
35
|
-
.map((d) => (typeof d?.score === 'number' ? d.score : null))
|
|
36
|
-
.filter((s) => s !== null)
|
|
37
|
-
return scores.length > 0 ? scores : null
|
|
38
|
-
}
|
|
39
|
-
if (typeof dims === 'object') {
|
|
40
|
-
const scores = []
|
|
41
|
-
for (const v of Object.values(dims)) {
|
|
42
|
-
if (typeof v === 'number') scores.push(v)
|
|
43
|
-
else if (typeof v?.score === 'number') scores.push(v.score)
|
|
44
|
-
}
|
|
45
|
-
return scores.length > 0 ? scores : null
|
|
46
|
-
}
|
|
47
|
-
return null
|
|
48
|
-
}
|
|
49
|
-
|
|
50
25
|
const gate = {
|
|
51
26
|
id: 'verify-approved',
|
|
52
|
-
description:
|
|
53
|
-
'qa_signoff.json exists with verdict === "approved" (or "approved-with-notes" when configured)',
|
|
27
|
+
description: 'qa_signoff.json exists with verdict === "approved" (v0.9.0+ two-gate verdict)',
|
|
54
28
|
severity: 'blocking',
|
|
55
29
|
skills: ['ship'],
|
|
56
30
|
applies_to_profile: (ctx) => ctx.profile === 'full-pipeline',
|
|
@@ -77,65 +51,32 @@ const gate = {
|
|
|
77
51
|
|
|
78
52
|
// BUG-013: route through parseQaSignoff so the verdict is
|
|
79
53
|
// case-insensitive AND tolerates `_↔-` (so 'APPROVED', 'Approved',
|
|
80
|
-
// ' approved ', 'APPROVED_WITH_NOTES' all behave canonically).
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
54
|
+
// ' approved ', 'APPROVED_WITH_NOTES' all behave canonically).
|
|
55
|
+
// v0.9.0: parseQaSignoff THROWS on legacy approved_with_notes —
|
|
56
|
+
// surface that as a gate fail with the migration message verbatim.
|
|
57
|
+
let parsed
|
|
58
|
+
try {
|
|
59
|
+
parsed = parseQaSignoff(doc)
|
|
60
|
+
} catch (err) {
|
|
86
61
|
return {
|
|
87
|
-
status: '
|
|
88
|
-
reason:
|
|
62
|
+
status: 'fail',
|
|
63
|
+
reason: `qa_signoff.json carries a legacy verdict that requires migration: ${err.message}`,
|
|
89
64
|
inputs,
|
|
90
65
|
}
|
|
91
66
|
}
|
|
67
|
+
const { verdict } = parsed
|
|
92
68
|
|
|
93
|
-
|
|
94
|
-
// in via config.qa.approved_with_notes_threshold. The threshold is
|
|
95
|
-
// the minimum per-dimension score required. Best-effort: when
|
|
96
|
-
// dimensions are missing / in an unknown shape we still accept and
|
|
97
|
-
// log, rather than hard-failing, so projects that haven't started
|
|
98
|
-
// scoring dimensions yet aren't blocked.
|
|
99
|
-
if (verdict === 'approved-with-notes') {
|
|
100
|
-
const threshold = ctx.config?.qa?.approved_with_notes_threshold
|
|
101
|
-
if (typeof threshold !== 'number') {
|
|
102
|
-
return {
|
|
103
|
-
status: 'fail',
|
|
104
|
-
reason:
|
|
105
|
-
'qa_signoff.json verdict is "approved-with-notes" but config.qa.approved_with_notes_threshold is not set',
|
|
106
|
-
evidence: { verdict, threshold: threshold ?? null },
|
|
107
|
-
inputs,
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const scores = extractDimensionScores(doc)
|
|
111
|
-
if (scores === null) {
|
|
112
|
-
return {
|
|
113
|
-
status: 'pass',
|
|
114
|
-
reason: `qa_signoff.json verdict is "approved-with-notes" and threshold ${threshold} is enabled; dimensions missing or unknown-shape — accepting best-effort`,
|
|
115
|
-
evidence: { verdict, threshold, dimensions_present: false },
|
|
116
|
-
inputs,
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const failing = scores.filter((s) => s < threshold)
|
|
120
|
-
if (failing.length > 0) {
|
|
121
|
-
return {
|
|
122
|
-
status: 'fail',
|
|
123
|
-
reason: `qa_signoff.json verdict is "approved-with-notes" but ${failing.length}/${scores.length} dimension score(s) below threshold ${threshold}`,
|
|
124
|
-
evidence: { verdict, threshold, scores, failing },
|
|
125
|
-
inputs,
|
|
126
|
-
}
|
|
127
|
-
}
|
|
69
|
+
if (verdict === 'approved') {
|
|
128
70
|
return {
|
|
129
71
|
status: 'pass',
|
|
130
|
-
reason:
|
|
131
|
-
evidence: { verdict, threshold, scores },
|
|
72
|
+
reason: 'qa_signoff.json verdict normalized to "approved"',
|
|
132
73
|
inputs,
|
|
133
74
|
}
|
|
134
75
|
}
|
|
135
76
|
|
|
136
77
|
return {
|
|
137
78
|
status: 'fail',
|
|
138
|
-
reason: `qa_signoff.json verdict is "${verdict}" (expected "approved"
|
|
79
|
+
reason: `qa_signoff.json verdict is "${verdict}" (expected "approved"; v0.9.0 enum is {approved, rejected, needs_human})`,
|
|
139
80
|
evidence: { verdict },
|
|
140
81
|
inputs,
|
|
141
82
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git/default-branch.mjs — local default-branch resolution + merged-PR parsing
|
|
3
|
+
* for the Team-Change Digest (LD-03/04/05).
|
|
4
|
+
*
|
|
5
|
+
* Pure git helpers (no .aperant IO). Every git call uses the no-throw
|
|
6
|
+
* execFileSync pattern from git/remote.mjs (stdio ['ignore','pipe','ignore'])
|
|
7
|
+
* so a non-git dir / unresolvable ref returns null instead of throwing.
|
|
8
|
+
*
|
|
9
|
+
* The resolver reads the LOCAL default-branch ref only — it NEVER calls
|
|
10
|
+
* `git fetch`. The digest surfaces work the user has actually pulled, not
|
|
11
|
+
* unfetched server state (ADR Consequences ¶1).
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from 'node:child_process'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run a git command, returning trimmed stdout or null on any error.
|
|
20
|
+
* @param {string} targetDir
|
|
21
|
+
* @param {string[]} args
|
|
22
|
+
* @returns {string|null}
|
|
23
|
+
*/
|
|
24
|
+
function git(targetDir, args) {
|
|
25
|
+
try {
|
|
26
|
+
return execFileSync('git', args, {
|
|
27
|
+
cwd: targetDir,
|
|
28
|
+
encoding: 'utf-8',
|
|
29
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
30
|
+
}).trim()
|
|
31
|
+
} catch {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the local default-branch ref, with its current sha. Resolution order:
|
|
38
|
+
* 1. `git symbolic-ref refs/remotes/origin/HEAD` (strip the
|
|
39
|
+
* 'refs/remotes/origin/' prefix);
|
|
40
|
+
* 2. `git config init.defaultBranch`;
|
|
41
|
+
* 3. existence check of `main` then `master` via `git rev-parse --verify`.
|
|
42
|
+
*
|
|
43
|
+
* NEVER fetches. Returns null outside a git repo or when nothing resolves.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} targetDir
|
|
46
|
+
* @returns {{ branch: string, sha: string }|null}
|
|
47
|
+
*/
|
|
48
|
+
export function resolveLocalDefaultBranch(targetDir) {
|
|
49
|
+
// Fast bail when not a git repo.
|
|
50
|
+
if (git(targetDir, ['rev-parse', '--git-dir']) === null) return null
|
|
51
|
+
|
|
52
|
+
let branch = null
|
|
53
|
+
|
|
54
|
+
// (1) origin/HEAD symbolic-ref — the authoritative "what main points at".
|
|
55
|
+
const originHead = git(targetDir, ['symbolic-ref', 'refs/remotes/origin/HEAD'])
|
|
56
|
+
if (originHead?.startsWith('refs/remotes/origin/')) {
|
|
57
|
+
branch = originHead.slice('refs/remotes/origin/'.length)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// (2) init.defaultBranch config.
|
|
61
|
+
if (!branch) {
|
|
62
|
+
const configured = git(targetDir, ['config', 'init.defaultBranch'])
|
|
63
|
+
if (configured) branch = configured
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// (3) existence check main → master.
|
|
67
|
+
if (!branch) {
|
|
68
|
+
for (const candidate of ['main', 'master']) {
|
|
69
|
+
if (git(targetDir, ['rev-parse', '--verify', '--quiet', candidate]) !== null) {
|
|
70
|
+
branch = candidate
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!branch) return null
|
|
77
|
+
|
|
78
|
+
const sha = git(targetDir, ['rev-parse', branch])
|
|
79
|
+
if (!sha) return null
|
|
80
|
+
|
|
81
|
+
return { branch, sha }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse a PR number + title from a merge-commit subject/body.
|
|
86
|
+
*
|
|
87
|
+
* GitHub merge-commit format:
|
|
88
|
+
* subject: "Merge pull request #N from owner/branch"
|
|
89
|
+
* body: "<PR title>" (first non-empty body line)
|
|
90
|
+
* Squash-merge format:
|
|
91
|
+
* subject: "<PR title> (#N)"
|
|
92
|
+
*
|
|
93
|
+
* @param {string} subject
|
|
94
|
+
* @param {string} body
|
|
95
|
+
* @returns {{ number: number, title: string }|null}
|
|
96
|
+
*/
|
|
97
|
+
function parsePullRequest(subject, body) {
|
|
98
|
+
const mergeMatch = subject.match(/^Merge pull request #(\d+) from /)
|
|
99
|
+
if (mergeMatch) {
|
|
100
|
+
const number = Number(mergeMatch[1])
|
|
101
|
+
const firstBodyLine = (body || '')
|
|
102
|
+
.split('\n')
|
|
103
|
+
.map((l) => l.trim())
|
|
104
|
+
.find((l) => l.length > 0)
|
|
105
|
+
return { number, title: firstBodyLine || subject }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// GitHub squash-merge convention: greedy `(.*)` captures the longest prefix
|
|
109
|
+
// so titles with parentheses (e.g. "feat(core): ...") are handled correctly.
|
|
110
|
+
const squashMatch = subject.match(/^(.*)\s\(#(\d+)\)\s*$/)
|
|
111
|
+
if (squashMatch) {
|
|
112
|
+
return { number: Number(squashMatch[2]), title: squashMatch[1].trim() }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve both the introduced-commit author emails (LD-05) and a display name
|
|
120
|
+
* from a single `git log` call per merge. Replaces the former separate
|
|
121
|
+
* `introducedAuthorEmails` + `pickAuthorName` pair (PRF-002).
|
|
122
|
+
*
|
|
123
|
+
* @param {string} targetDir
|
|
124
|
+
* @param {string[]} parents parent shas of the merge commit
|
|
125
|
+
* @param {string} authorEmail the merge commit's own author email
|
|
126
|
+
* @param {string} currentUserEmail the user's git email (for name picking)
|
|
127
|
+
* @param {string} fallbackName merge/commit author name when no better pick
|
|
128
|
+
* @returns {{ emails: string[], authorName: string }}
|
|
129
|
+
*/
|
|
130
|
+
function resolveIntroducedAuthors(targetDir, parents, authorEmail, currentUserEmail, fallbackName) {
|
|
131
|
+
if (parents.length >= 2) {
|
|
132
|
+
const raw = git(targetDir, ['log', `${parents[0]}..${parents[1]}`, '--format=%ae%x1f%an'])
|
|
133
|
+
if (raw) {
|
|
134
|
+
const emails = new Set()
|
|
135
|
+
let pickedName = null
|
|
136
|
+
for (const line of raw.split('\n')) {
|
|
137
|
+
const [email, name] = line.split('\x1f')
|
|
138
|
+
if (!email) continue
|
|
139
|
+
const normEmail = email.trim().toLowerCase()
|
|
140
|
+
if (normEmail.length > 0) emails.add(normEmail)
|
|
141
|
+
if (!pickedName && normEmail !== currentUserEmail) {
|
|
142
|
+
pickedName = (name || fallbackName).trim()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (emails.size > 0) {
|
|
146
|
+
return { emails: [...emails], authorName: pickedName || fallbackName.trim() }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const own = authorEmail.trim().toLowerCase()
|
|
151
|
+
return {
|
|
152
|
+
emails: own.length > 0 ? [own] : [],
|
|
153
|
+
authorName: fallbackName.trim(),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* List merged PRs on the local default branch since `sinceSha`, attributed to
|
|
159
|
+
* whoever actually wrote the work (LD-04/05).
|
|
160
|
+
*
|
|
161
|
+
* Walks `git log --first-parent <sinceSha>..<defaultBranch>`. Each commit that
|
|
162
|
+
* parses as a PR (merge-commit or squash form) becomes one entry, attributed
|
|
163
|
+
* via its introduced-commit author emails. An entry is excluded only when ALL
|
|
164
|
+
* its introduced emails equal `currentUserEmail` (the user's own work) — so a
|
|
165
|
+
* PR the user merged but a teammate authored still appears.
|
|
166
|
+
*
|
|
167
|
+
* The scan is bounded (R3): at most `maxEntries * SCAN_MULTIPLIER` commits are
|
|
168
|
+
* inspected, so a stale marker can't enumerate an unbounded history.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} targetDir
|
|
171
|
+
* @param {object} opts
|
|
172
|
+
* @param {string} opts.sinceSha baseline sha (exclusive lower bound)
|
|
173
|
+
* @param {string} [opts.defaultBranch] branch to walk to (default: resolved)
|
|
174
|
+
* @param {string} opts.currentUserEmail the user's git email (own-work filter)
|
|
175
|
+
* @param {number} [opts.maxEntries=7] cap on returned entries
|
|
176
|
+
* @returns {{ entries: Array<{ number: number, title: string, author: string }>, more_count: number }}
|
|
177
|
+
*/
|
|
178
|
+
export function listMergedPullRequests(targetDir, opts = {}) {
|
|
179
|
+
const empty = { entries: [], more_count: 0, more_capped: false }
|
|
180
|
+
const sinceSha = opts.sinceSha
|
|
181
|
+
const maxEntries = Number.isInteger(opts.maxEntries) && opts.maxEntries > 0 ? opts.maxEntries : 7
|
|
182
|
+
const currentUserEmail = (opts.currentUserEmail || '').trim().toLowerCase()
|
|
183
|
+
|
|
184
|
+
let defaultBranch = opts.defaultBranch
|
|
185
|
+
if (!defaultBranch) {
|
|
186
|
+
const resolved = resolveLocalDefaultBranch(targetDir)
|
|
187
|
+
if (!resolved) return empty
|
|
188
|
+
defaultBranch = resolved.branch
|
|
189
|
+
}
|
|
190
|
+
if (!sinceSha) return empty
|
|
191
|
+
if (!/^[0-9a-f]{7,40}$/i.test(sinceSha)) return empty
|
|
192
|
+
|
|
193
|
+
const SCAN_MULTIPLIER = 4
|
|
194
|
+
const scanCap = maxEntries * SCAN_MULTIPLIER
|
|
195
|
+
|
|
196
|
+
// One commit per record, fields separated by \x1f, records by \x1e.
|
|
197
|
+
// %H sha · %P parents · %an author name · %ae author email · %s subject · %b body
|
|
198
|
+
const FIELD = '\x1f'
|
|
199
|
+
const RECORD = '\x1e'
|
|
200
|
+
const format = `%H${FIELD}%P${FIELD}%an${FIELD}%ae${FIELD}%s${FIELD}%b${RECORD}`
|
|
201
|
+
const raw = git(targetDir, [
|
|
202
|
+
'log',
|
|
203
|
+
'--first-parent',
|
|
204
|
+
`--max-count=${scanCap}`,
|
|
205
|
+
`--format=${format}`,
|
|
206
|
+
`${sinceSha}..${defaultBranch}`,
|
|
207
|
+
])
|
|
208
|
+
if (!raw) return empty
|
|
209
|
+
|
|
210
|
+
const records = raw
|
|
211
|
+
.split(RECORD)
|
|
212
|
+
.map((r) => r.replace(/^\n+/, ''))
|
|
213
|
+
.filter((r) => r.trim().length > 0)
|
|
214
|
+
|
|
215
|
+
const matched = []
|
|
216
|
+
for (const rec of records) {
|
|
217
|
+
const [sha, parentsRaw, authorName, authorEmail, subject, body] = rec.split(FIELD)
|
|
218
|
+
if (!sha) continue
|
|
219
|
+
const pr = parsePullRequest(subject || '', body || '')
|
|
220
|
+
if (!pr) continue
|
|
221
|
+
|
|
222
|
+
const parents = (parentsRaw || '').trim().split(/\s+/).filter(Boolean)
|
|
223
|
+
const { emails, authorName: resolvedAuthor } = resolveIntroducedAuthors(
|
|
224
|
+
targetDir,
|
|
225
|
+
parents,
|
|
226
|
+
authorEmail || '',
|
|
227
|
+
currentUserEmail,
|
|
228
|
+
authorName || 'unknown',
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
// Exclude only when EVERY introduced commit is the current user's own.
|
|
232
|
+
const allOwn =
|
|
233
|
+
currentUserEmail.length > 0 &&
|
|
234
|
+
emails.length > 0 &&
|
|
235
|
+
emails.every((e) => e === currentUserEmail)
|
|
236
|
+
if (allOwn) continue
|
|
237
|
+
|
|
238
|
+
matched.push({ number: pr.number, title: pr.title, author: resolvedAuthor })
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const more_capped = records.length >= scanCap
|
|
242
|
+
if (matched.length <= maxEntries) {
|
|
243
|
+
return { entries: matched, more_count: 0, more_capped }
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
entries: matched.slice(0, maxEntries),
|
|
247
|
+
more_count: matched.length - maxEntries,
|
|
248
|
+
more_capped,
|
|
249
|
+
}
|
|
250
|
+
}
|
package/src/cli/git/identity.mjs
CHANGED
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import { execSync } from 'node:child_process'
|
|
6
6
|
|
|
7
|
+
const _identityCache = new Map()
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Auto-detect the current user's git identity. Returns null if git config is unset.
|
|
11
|
+
* Memoized per targetDir for the process lifetime (git identity is stable within a CLI invocation).
|
|
9
12
|
*/
|
|
10
13
|
export function getGitIdentity(targetDir) {
|
|
14
|
+
if (_identityCache.has(targetDir)) return _identityCache.get(targetDir)
|
|
15
|
+
let result = null
|
|
11
16
|
try {
|
|
12
17
|
const name = execSync('git config user.name 2>/dev/null', {
|
|
13
18
|
cwd: targetDir,
|
|
@@ -17,11 +22,12 @@ export function getGitIdentity(targetDir) {
|
|
|
17
22
|
cwd: targetDir,
|
|
18
23
|
encoding: 'utf-8',
|
|
19
24
|
}).trim()
|
|
20
|
-
if (
|
|
21
|
-
return { name, email }
|
|
25
|
+
if (name && email) result = { name, email }
|
|
22
26
|
} catch {
|
|
23
|
-
|
|
27
|
+
/* no git config — result stays null */
|
|
24
28
|
}
|
|
29
|
+
_identityCache.set(targetDir, result)
|
|
30
|
+
return result
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/**
|
|
@@ -88,6 +88,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
|
|
|
88
88
|
'agents/apt.md',
|
|
89
89
|
'agents/apt-author-skill.md',
|
|
90
90
|
'agents/apt-bootstrap.md',
|
|
91
|
+
'agents/apt-catch-up.md',
|
|
91
92
|
'agents/apt-caveman.md',
|
|
92
93
|
'agents/apt-classify.md',
|
|
93
94
|
'agents/apt-close-task.md',
|
|
@@ -131,6 +132,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
|
|
|
131
132
|
'commands/apt.md',
|
|
132
133
|
'commands/apt-author-skill.md',
|
|
133
134
|
'commands/apt-bootstrap.md',
|
|
135
|
+
'commands/apt-catch-up.md',
|
|
134
136
|
'commands/apt-caveman.md',
|
|
135
137
|
'commands/apt-classify.md',
|
|
136
138
|
'commands/apt-close-task.md',
|
|
@@ -61,6 +61,17 @@ export const PROVISION_SPECS = {
|
|
|
61
61
|
args: ['./scripts/tauri-mcp.sh'],
|
|
62
62
|
requiresLauncherScript: true,
|
|
63
63
|
},
|
|
64
|
+
'mobile-mcp': {
|
|
65
|
+
kind: 'mcp-server',
|
|
66
|
+
// Unified iOS + Android UI-driving server (Apache-2.0). Pure npm —
|
|
67
|
+
// the iOS/Android SYSTEM prereqs (idb-companion / adb / Xcode /
|
|
68
|
+
// Android SDK) are validated by the mobile driver's preflight, NOT
|
|
69
|
+
// installed here (decision D-09). Matches the driver manifest's
|
|
70
|
+
// transport.mcp_server_id = "mobile-mcp".
|
|
71
|
+
command: 'npx',
|
|
72
|
+
args: ['-y', '@mobilenext/mobile-mcp@latest'],
|
|
73
|
+
env: {},
|
|
74
|
+
},
|
|
64
75
|
// kind 'skill'
|
|
65
76
|
'agent-browser': {
|
|
66
77
|
kind: 'skill',
|
|
@@ -79,14 +90,15 @@ export const PROVISION_SPECS = {
|
|
|
79
90
|
* triggers. A multi-surface monorepo (e.g. electron + nextjs) returns
|
|
80
91
|
* multiple ids.
|
|
81
92
|
*
|
|
82
|
-
* is_electron
|
|
83
|
-
* is_tauri
|
|
84
|
-
* is_web_frontend|is_nextjs|is_nuxt
|
|
93
|
+
* is_electron → ['electron']
|
|
94
|
+
* is_tauri → ['tauri']
|
|
95
|
+
* is_web_frontend|is_nextjs|is_nuxt → ['agent-browser']
|
|
96
|
+
* is_ios_native|is_android_native|is_expo|is_react_native → ['mobile-mcp']
|
|
85
97
|
*
|
|
86
98
|
* Accepts a partial capability record — only the flags it reads need be
|
|
87
99
|
* present (the rest default to absent/false).
|
|
88
100
|
*
|
|
89
|
-
* @param {Partial<import('../util/runtime-capabilities.mjs').RuntimeCapabilities>} runtimeCaps
|
|
101
|
+
* @param {Partial<import('../util/runtime-capabilities.mjs').RuntimeCapabilities> | null | undefined} runtimeCaps
|
|
90
102
|
* @returns {string[]}
|
|
91
103
|
*/
|
|
92
104
|
export function capabilityToProvisionIds(runtimeCaps) {
|
|
@@ -97,5 +109,13 @@ export function capabilityToProvisionIds(runtimeCaps) {
|
|
|
97
109
|
if (runtimeCaps.is_web_frontend || runtimeCaps.is_nextjs || runtimeCaps.is_nuxt) {
|
|
98
110
|
ids.push('agent-browser')
|
|
99
111
|
}
|
|
112
|
+
if (
|
|
113
|
+
runtimeCaps.is_ios_native ||
|
|
114
|
+
runtimeCaps.is_android_native ||
|
|
115
|
+
runtimeCaps.is_expo ||
|
|
116
|
+
runtimeCaps.is_react_native
|
|
117
|
+
) {
|
|
118
|
+
ids.push('mobile-mcp')
|
|
119
|
+
}
|
|
100
120
|
return ids
|
|
101
121
|
}
|