@groundnuty/macf 0.2.10 → 0.2.12
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/dist/.build-info.json +2 -2
- package/dist/cli/claude-sh.d.ts.map +1 -1
- package/dist/cli/claude-sh.js +139 -33
- package/dist/cli/claude-sh.js.map +1 -1
- package/dist/cli/commands/init.d.ts +36 -3
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +190 -10
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrate.d.ts +44 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -0
- package/dist/cli/commands/migrate.js +119 -0
- package/dist/cli/commands/migrate.js.map +1 -0
- package/dist/cli/config.d.ts +10 -2
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +16 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/index.js +21 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/registry-helper.d.ts.map +1 -1
- package/dist/cli/registry-helper.js +8 -0
- package/dist/cli/registry-helper.js.map +1 -1
- package/dist/cli/settings-writer.d.ts +21 -4
- package/dist/cli/settings-writer.d.ts.map +1 -1
- package/dist/cli/settings-writer.js +26 -4
- package/dist/cli/settings-writer.js.map +1 -1
- package/dist/plugin/bin/macf-plugin-cli.js +6 -4
- package/dist/plugin/bin/macf-plugin-cli.js.map +1 -1
- package/dist/plugin/lib/build-dashboard-health.d.ts +23 -0
- package/dist/plugin/lib/build-dashboard-health.d.ts.map +1 -0
- package/dist/plugin/lib/build-dashboard-health.js +23 -0
- package/dist/plugin/lib/build-dashboard-health.js.map +1 -0
- package/dist/plugin/lib/index.d.ts +2 -0
- package/dist/plugin/lib/index.d.ts.map +1 -1
- package/dist/plugin/lib/index.js +2 -0
- package/dist/plugin/lib/index.js.map +1 -1
- package/dist/plugin/lib/probe-peer-health.d.ts +20 -0
- package/dist/plugin/lib/probe-peer-health.d.ts.map +1 -0
- package/dist/plugin/lib/probe-peer-health.js +40 -0
- package/dist/plugin/lib/probe-peer-health.js.map +1 -0
- package/package.json +2 -2
- package/plugin/rules/gh-token-attribution-traps.md +12 -0
- package/plugin/rules/pr-discipline.md +32 -0
- package/plugin/rules/silent-fallback-hazards.md +7 -1
- package/scripts/check-lgtm-gate.sh +274 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"macf-plugin-cli.js","sourceRoot":"","sources":["../../../src/plugin/bin/macf-plugin-cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;GASG;AACH,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACtG,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG1D,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAEhC,SAAS,iBAAiB;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAClD,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAChD,IAAI,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAChD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAClD,IAAI,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IACvD,mBAAmB;IACnB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC7D,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,SAAS,CAAC;IAC9D,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,MAAM,CAAC;IACtD,MAAM,cAAc,GAAG,iBAAiB,EAAE,CAAC;IAE3C,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,KAAK,GAAG,MAAM,aAAa,EAAE,CAAC;YACpC,MAAM,QAAQ,GAAG,wBAAwB,CAAC,cAAc,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;YAC1E,mEAAmE;YACnE,gEAAgE;YAChE,6DAA6D;YAC7D,MAAM,CAAC,eAAe,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBACjD,kBAAkB,CAAC,SAAS,EAAE,QAAQ,CAAC;gBACvC,SAAS,CAAC,QAAQ,CAAC;aACpB,CAAC,CAAC;YACH,
|
|
1
|
+
{"version":3,"file":"macf-plugin-cli.js","sourceRoot":"","sources":["../../../src/plugin/bin/macf-plugin-cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HealthResponse } from '@groundnuty/macf-core';
|
|
2
|
+
import type { OwnRegistration, PeerEntry } from './registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build the `ownHealth` + `peersWithHealth` arguments for `formatDashboard`
|
|
5
|
+
* by probing each registered peer (and self if registered) over mTLS via
|
|
6
|
+
* the injected probe function.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from `macf-plugin-cli.ts` `status` case to make the wiring
|
|
9
|
+
* testable without spawning the full binary. Probe dependency is injected
|
|
10
|
+
* so tests can supply a stub instead of `probePeerHealth`.
|
|
11
|
+
*
|
|
12
|
+
* Surfaced by macf#327 — `status` case was previously a stub mapping
|
|
13
|
+
* every peer to `health: null` (sister-class to the `peers` stub fixed in
|
|
14
|
+
* macf#325 / PR #326).
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildDashboardHealth(ownRegistration: OwnRegistration | null, peers: readonly PeerEntry[], probe: (peer: PeerEntry) => Promise<HealthResponse | null>): Promise<{
|
|
17
|
+
readonly ownHealth: HealthResponse | null;
|
|
18
|
+
readonly peersWithHealth: ReadonlyArray<{
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly health: HealthResponse | null;
|
|
21
|
+
}>;
|
|
22
|
+
}>;
|
|
23
|
+
//# sourceMappingURL=build-dashboard-health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-dashboard-health.d.ts","sourceRoot":"","sources":["../../../src/plugin/lib/build-dashboard-health.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CACxC,eAAe,EAAE,eAAe,GAAG,IAAI,EACvC,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,KAAK,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,GACzD,OAAO,CAAC;IACT,QAAQ,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAAC;IAC1C,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC5G,CAAC,CAQD"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the `ownHealth` + `peersWithHealth` arguments for `formatDashboard`
|
|
3
|
+
* by probing each registered peer (and self if registered) over mTLS via
|
|
4
|
+
* the injected probe function.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `macf-plugin-cli.ts` `status` case to make the wiring
|
|
7
|
+
* testable without spawning the full binary. Probe dependency is injected
|
|
8
|
+
* so tests can supply a stub instead of `probePeerHealth`.
|
|
9
|
+
*
|
|
10
|
+
* Surfaced by macf#327 — `status` case was previously a stub mapping
|
|
11
|
+
* every peer to `health: null` (sister-class to the `peers` stub fixed in
|
|
12
|
+
* macf#325 / PR #326).
|
|
13
|
+
*/
|
|
14
|
+
export async function buildDashboardHealth(ownRegistration, peers, probe) {
|
|
15
|
+
const [ownHealth, peersWithHealth] = await Promise.all([
|
|
16
|
+
ownRegistration
|
|
17
|
+
? probe({ name: ownRegistration.name, info: ownRegistration.info })
|
|
18
|
+
: Promise.resolve(null),
|
|
19
|
+
Promise.all(peers.map(async (p) => ({ name: p.name, health: await probe(p) }))),
|
|
20
|
+
]);
|
|
21
|
+
return { ownHealth, peersWithHealth };
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=build-dashboard-health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-dashboard-health.js","sourceRoot":"","sources":["../../../src/plugin/lib/build-dashboard-health.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,eAAuC,EACvC,KAA2B,EAC3B,KAA0D;IAK1D,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACrD,eAAe;YACb,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,eAAe,CAAC,IAAI,EAAE,CAAC;YACnE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAC,CAAC,EAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;KAC9E,CAAC,CAAC;IACH,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;AACxC,CAAC"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { getOwnRegistration, listPeers } from './registry.js';
|
|
2
2
|
export type { OwnRegistration, PeerEntry } from './registry.js';
|
|
3
3
|
export { pingAgent } from './health.js';
|
|
4
|
+
export { probePeerHealth } from './probe-peer-health.js';
|
|
5
|
+
export { buildDashboardHealth } from './build-dashboard-health.js';
|
|
4
6
|
export { checkIssues } from './work.js';
|
|
5
7
|
export type { PendingIssue } from './work.js';
|
|
6
8
|
export { formatDashboard, formatPeerTable, formatIssues } from './format.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugin/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC9D,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugin/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC9D,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/plugin/lib/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { getOwnRegistration, listPeers } from './registry.js';
|
|
2
2
|
export { pingAgent } from './health.js';
|
|
3
|
+
export { probePeerHealth } from './probe-peer-health.js';
|
|
4
|
+
export { buildDashboardHealth } from './build-dashboard-health.js';
|
|
3
5
|
export { checkIssues } from './work.js';
|
|
4
6
|
export { formatDashboard, formatPeerTable, formatIssues } from './format.js';
|
|
5
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/plugin/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE9D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/plugin/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE9D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PeerEntry } from './registry.js';
|
|
2
|
+
import type { HealthResponse } from '@groundnuty/macf-core';
|
|
3
|
+
/**
|
|
4
|
+
* Probe a peer's `/health` endpoint over mTLS using the cert paths set by
|
|
5
|
+
* `claude.sh` (MACF_CA_CERT / MACF_AGENT_CERT / MACF_AGENT_KEY env vars).
|
|
6
|
+
*
|
|
7
|
+
* Returns `null` when env vars are missing or CA-cert read fails — caller's
|
|
8
|
+
* UI layer renders that as "offline" (matches `formatPeerTable` behaviour).
|
|
9
|
+
*
|
|
10
|
+
* Used by the `peers` and `status` cases in `macf-plugin-cli.ts`. The `ping`
|
|
11
|
+
* case keeps its own inline copy because it has a different UX contract:
|
|
12
|
+
* operator-invoked `/macf-ping` should fail loudly when env is incomplete,
|
|
13
|
+
* not silently render "offline".
|
|
14
|
+
*
|
|
15
|
+
* Surfaced by macf#325 — `peers` case was previously a stub mapping every
|
|
16
|
+
* peer to `health: null`, producing misleading "all offline" output even
|
|
17
|
+
* when channel-servers were running. This helper is the structural fix.
|
|
18
|
+
*/
|
|
19
|
+
export declare function probePeerHealth(peer: PeerEntry): Promise<HealthResponse | null>;
|
|
20
|
+
//# sourceMappingURL=probe-peer-health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe-peer-health.d.ts","sourceRoot":"","sources":["../../../src/plugin/lib/probe-peer-health.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAkBrF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { pingAgent } from './health.js';
|
|
3
|
+
/**
|
|
4
|
+
* Probe a peer's `/health` endpoint over mTLS using the cert paths set by
|
|
5
|
+
* `claude.sh` (MACF_CA_CERT / MACF_AGENT_CERT / MACF_AGENT_KEY env vars).
|
|
6
|
+
*
|
|
7
|
+
* Returns `null` when env vars are missing or CA-cert read fails — caller's
|
|
8
|
+
* UI layer renders that as "offline" (matches `formatPeerTable` behaviour).
|
|
9
|
+
*
|
|
10
|
+
* Used by the `peers` and `status` cases in `macf-plugin-cli.ts`. The `ping`
|
|
11
|
+
* case keeps its own inline copy because it has a different UX contract:
|
|
12
|
+
* operator-invoked `/macf-ping` should fail loudly when env is incomplete,
|
|
13
|
+
* not silently render "offline".
|
|
14
|
+
*
|
|
15
|
+
* Surfaced by macf#325 — `peers` case was previously a stub mapping every
|
|
16
|
+
* peer to `health: null`, producing misleading "all offline" output even
|
|
17
|
+
* when channel-servers were running. This helper is the structural fix.
|
|
18
|
+
*/
|
|
19
|
+
export async function probePeerHealth(peer) {
|
|
20
|
+
const caCertPath = process.env['MACF_CA_CERT'];
|
|
21
|
+
const agentCertPath = process.env['MACF_AGENT_CERT'];
|
|
22
|
+
const agentKeyPath = process.env['MACF_AGENT_KEY'];
|
|
23
|
+
if (!caCertPath || !agentCertPath || !agentKeyPath)
|
|
24
|
+
return null;
|
|
25
|
+
let caCertPem;
|
|
26
|
+
try {
|
|
27
|
+
caCertPem = readFileSync(caCertPath, 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return await pingAgent({
|
|
33
|
+
host: peer.info.host,
|
|
34
|
+
port: peer.info.port,
|
|
35
|
+
caCertPem,
|
|
36
|
+
certPath: agentCertPath,
|
|
37
|
+
keyPath: agentKeyPath,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=probe-peer-health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe-peer-health.js","sourceRoot":"","sources":["../../../src/plugin/lib/probe-peer-health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAIxC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAe;IACnD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC/C,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACrD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACnD,IAAI,CAAC,UAAU,IAAI,CAAC,aAAa,IAAI,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IAChE,IAAI,SAAiB,CAAC;IACtB,IAAI,CAAC;QACH,SAAS,GAAG,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAM,SAAS,CAAC;QACrB,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI;QACpB,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI;QACpB,SAAS;QACT,QAAQ,EAAE,aAAa;QACvB,OAAO,EAAE,YAAY;KACtB,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groundnuty/macf",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Multi-Agent Coordination Framework CLI — coordinate Claude Code agents via GitHub. Installs as `macf` binary; use `macf init` to set up an agent workspace, `macf update` to refresh rules + version pins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"test:watch": "vitest"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@groundnuty/macf-core": "0.2.
|
|
38
|
+
"@groundnuty/macf-core": "0.2.12",
|
|
39
39
|
"commander": "^14.0.3",
|
|
40
40
|
"reflect-metadata": "^0.2.2",
|
|
41
41
|
"zod": "^4.0.0"
|
|
@@ -143,6 +143,18 @@ When intentionally bypassing the hook for a knowingly user-attributed op (e.g.,
|
|
|
143
143
|
|
|
144
144
|
---
|
|
145
145
|
|
|
146
|
+
## Structural backstop: in-runner token refresh in macf-channel-server (macf#317)
|
|
147
|
+
|
|
148
|
+
The 6 failure modes above all describe **token-acquisition** (the new token coming back wrong); macf#317 surfaced a 7th class — **token-expiry** during long-running sessions. Bot installation tokens have a 1-hour TTL by design; `claude.sh` mints a fresh token at session start and exports `GH_TOKEN`, but **macf-channel-server inherits this fixed token via `process.env` and has no in-runner refresh** absent the macf#317 fix. After 1 hour, every gh-API-using handler (notify_peer's registry lookups, /sign's varsClient calls) 401s. Witnessed 2026-05-01: cv-architect Stop hook 401 at ~67min uptime.
|
|
149
|
+
|
|
150
|
+
The expiry sub-case is **shape-distinct** from modes 1–6: the token is `ghs_*` (right prefix), it was generated correctly, the helper script is installed, the path resolves — it's just stale. Helper-prefix-checking + PreToolUse hooks don't catch it; the structural fix is in-process refresh.
|
|
151
|
+
|
|
152
|
+
**v0.2.11+ defense:** macf-channel-server's `token-refresh.ts` + `refresh-aware-client.ts` modules. The token-refresher caches the current token in-process for ~50 minutes (10-min safety margin under 1hr TTL); on every gh-API call, the refresh-aware client gets a current token (cached or fresh); on 401, the wrapper force-refreshes + retries once. Refresh failures throw with diagnostic — channel-server fails loud rather than silently using a stale token. Implementation cites `silent-fallback-hazards.md Instance 1 (expiry sub-case)` in the diagnostic message so 401s in the field surface the canonical reference.
|
|
153
|
+
|
|
154
|
+
**Operator note:** the structural defense is automatic for consumer-fleet workspaces (npm-installed `@groundnuty/macf-channel-server@0.2.11+`). Substrate workspaces / bash-terminal sessions still need the operator-discipline pattern in `gh-token-refresh.md` ("refresh before every `gh` or `git push`").
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
146
158
|
## How this relates to other canonical rules
|
|
147
159
|
|
|
148
160
|
- `coordination.md` § "Token & Git Hygiene" documents the canonical helper invocation; this rule provides the failure-mode catalog the helper is designed to defend against.
|
|
@@ -350,6 +350,38 @@ changed.
|
|
|
350
350
|
|
|
351
351
|
---
|
|
352
352
|
|
|
353
|
+
## Structural enforcement — `check-lgtm-gate.sh` PreToolUse hook
|
|
354
|
+
|
|
355
|
+
Per `groundnuty/macf#270` (DR-023 UC-2), the "no LGTM = no merge" rule (codified above in §"How to submit LGTM" + §"When the reviewer is absent or unreachable" + §"Merge-by-implementer") is enforced by a Claude Code PreToolUse hook on `Bash` tool calls. The hook intercepts `gh pr merge` invocations, queries the target PR's review state via `gh pr view --json author,reviews`, and BLOCKs (`exit 2` with stderr explanation) when no APPROVED review from a non-author exists.
|
|
356
|
+
|
|
357
|
+
**Architectural shape (DR-023 amendment 2026-04-27):** this is a bash command-type hook, NOT `type: "mcp_tool"`. PreToolUse-blocking semantics + mcp_tool's non-blocking-on-disconnect failure mode are structurally incompatible — a missing or transiently-disconnected MCP server would silently allow the merge. Bash form fires uniformly across substrate workspaces (where the `macf-agent` MCP server is permanently off per operator directive 2026-04-27) AND consumer workspaces (startup window + transient disconnect periods produce silent-fail paths under mcp_tool). Same reasoning UC-4 (`check-mention-routing.sh`, PR #275) demonstrated empirically.
|
|
358
|
+
|
|
359
|
+
**The hook is the same shape as `check-gh-token.sh` (#140) + `check-mention-routing.sh` (#244 + #272):** a bash script distributed via `macf init` / `macf update` / `macf rules refresh` to every workspace's `.claude/scripts/check-lgtm-gate.sh` with the entry registered in `.claude/settings.json` `hooks.PreToolUse`. Substrate workspaces, tester agents, CV consumers, and future MACF-consumer projects all get the protection uniformly.
|
|
360
|
+
|
|
361
|
+
**Decision rule** (subject to refinement; documented for transparency):
|
|
362
|
+
|
|
363
|
+
- Command does NOT match `gh pr merge` (exact subcommand, wrapper-aware) → allow (other gh subcommands pass through unchanged)
|
|
364
|
+
- Command matches but no PR number can be extracted → allow (defense-in-depth; gh itself surfaces the usage error)
|
|
365
|
+
- Command matches AND `gh pr view --json author,reviews` returns at least one APPROVED review where reviewer login != author login (after normalizing `app/` prefix + `[bot]` suffix) → allow
|
|
366
|
+
- Command matches AND no non-author APPROVED review exists → BLOCK with stderr citing this rule + the missing-LGTM diagnosis + the `MACF_SKIP_LGTM_CHECK=1` operator override
|
|
367
|
+
- Any infrastructure failure (gh missing, network 404/5xx, malformed JSON) → fail-open (allow). Same defense-in-depth posture as `check-gh-token.sh` and `check-mention-routing.sh` — the hook closes residual policy slips, not all merge paths
|
|
368
|
+
|
|
369
|
+
**Wrapper coverage:** the regex matches `gh pr merge` preceded by `sudo`, `env VAR=...`, `watch`, `ionice`, `setsid`, `nice`, `time`, bare-`VAR=...` env-prefix forms, and shell wrappers (`bash -c "gh pr merge ..."`, `sh -c`, `zsh -c`, including flag-prefixed forms like `bash -lc`). Same wrapper allow-list as `check-gh-token.sh` + `check-mention-routing.sh`. Chained-form leadins `;` `|` `&&` covered.
|
|
370
|
+
|
|
371
|
+
**False-positive trade-off:** the hook leans toward false-positive over false-negative on parse-failure paths. PR-number extraction tolerates URL form (`https://github.com/owner/repo/pull/N`), `owner/repo#N` shorthand, and quoted positionals. Bare `gh pr merge` (no positional, gh prompts interactively) passes through unblocked — the operator's discipline catches it because the prompt is visible.
|
|
372
|
+
|
|
373
|
+
**Override (`MACF_SKIP_LGTM_CHECK=1`)** is the escape hatch for legitimate exceptions documented in §"When the reviewer is absent or unreachable":
|
|
374
|
+
|
|
375
|
+
- Reporter-sanctioned self-merge after extended reviewer absence
|
|
376
|
+
- The "PR author offline → reviewer merges with hand-off comment" exception
|
|
377
|
+
- Urgent reverts where waiting for review is itself a hazard
|
|
378
|
+
|
|
379
|
+
Per the `check-gh-token.sh` precedent, structural enforcement plus an escape hatch outperforms behavioral discipline alone.
|
|
380
|
+
|
|
381
|
+
**Empirical motivation:** `groundnuty/macf-testbed#229` Phase C iter 4 sweep (2026-04-26) recorded a self-merge-without-LGTM incident — a tester self-merged when its harness driver (acting as the de-facto reviewer) was killed mid-poll. Codified in §"When the reviewer is absent or unreachable" same day. The hook closes the residual: rule-discipline catches most cases; structural enforcement catches the slips that remain.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
353
385
|
## When to modify this rule
|
|
354
386
|
|
|
355
387
|
- **Read:** every session start.
|
|
@@ -30,6 +30,12 @@ The trap is that defensive programming targets exit codes, but exit-code success
|
|
|
30
30
|
**Recurrence:** 5+ confirmed instances across multiple agents
|
|
31
31
|
**Canonical defense:** `gh-token-attribution-traps.md` (sister canonical rule) — 6 specific failure modes + result-invariant defenses (`[[ "$GH_TOKEN" == ghs_* ]]` prefix check, `macf-whoami.sh` spot-check, PreToolUse hook intercepts `gh` and `git push` invocations)
|
|
32
32
|
|
|
33
|
+
**Sub-case (expiry, macf#317, 2026-05-01):** Bot installation tokens have a **1-hour TTL by design** (GitHub App contract). `claude.sh` mints a fresh token at session start and exports `GH_TOKEN`; the macf-channel-server child process inherits that same env var via standard Node `process.env` pass-through. **Without an in-runner refresh, every gh-API-using handler 401s after ~60 minutes of session uptime.** Detection: 401 (`Bad credentials`) on `listVariables` / `readVariable` / `writeVariable` calls 60+ min after server start. Witnessed 2026-05-01 ~14:30Z on cv-architect's Stop hook at ~67min uptime — the operator-witnessed incident motivating macf#317.
|
|
34
|
+
|
|
35
|
+
This is a distinct sub-case from the missing-helper / mis-pipefail / wrong-prefix attribution-trap modes catalogued in `gh-token-attribution-traps.md`: the agent has the right token *shape* (`ghs_*`) but it's stale. The hazard is structural — session lifetime ≠ token lifetime by design (1hr vs ∞), so any long-running agent eventually hits this.
|
|
36
|
+
|
|
37
|
+
**Defense (macf#317, v0.2.11):** in-process token-refresh helper in macf-channel-server (`src/token-refresh.ts`) caches `{token, mintedAt}`; on every `getRefreshedToken()` call, returns cached if `age < 50min` (10-min safety margin under 1hr TTL), else mints fresh via `macf-gh-token.sh`. The refresh-aware client wrapper (`src/refresh-aware-client.ts`) decorates `GitHubVariablesClient`: every method call gets a refreshed token; on 401, force-refreshes + retries once. Refresh failures throw with diagnostic — never silently fall back to the stale env-var token. Sister-shape: `macf-testbed#135` (sweep-harness in-runner refresh between iterations; closed/deferred for testbed but the channel-server class needed structural defense).
|
|
38
|
+
|
|
33
39
|
### Instance 2 — GitHub auto-close negation-blindness
|
|
34
40
|
|
|
35
41
|
**Surface:** PR / issue body markdown parsing
|
|
@@ -245,7 +251,7 @@ For coordination-system safety analysis: this is a class of hazards multi-agent
|
|
|
245
251
|
|
|
246
252
|
| Instance | Surface | Structural defense | Pattern |
|
|
247
253
|
|---|---|---|---|
|
|
248
|
-
| 1 — gh-token attribution traps | `gh` ops + bot tokens | PreToolUse hook + helper-with-fail-loud-prefix-check | Pattern B |
|
|
254
|
+
| 1 — gh-token attribution traps | `gh` ops + bot tokens | PreToolUse hook + helper-with-fail-loud-prefix-check; expiry sub-case (macf#317) adds in-runner token refresh in macf-channel-server (`token-refresh.ts` + `refresh-aware-client.ts`) — caches token ~50min, force-refreshes on 401 | Pattern B (acquisition) + Pattern A (expiry retry) |
|
|
249
255
|
| 2 — GitHub auto-close negation-blindness | PR/issue body markdown | Pattern B candidate; structural defense via PreToolUse hook on body content per #275 precedent — not yet shipped | Pattern B (latent) |
|
|
250
256
|
| 3 — Remote Control IPC blocking tmux send-keys | Claude Code TUI input | Two-tier: consumer fleet structurally retired via channel-server primitive (DR-020 mTLS HTTPS POST); substrate fleet permanent operational reality — defense = rule-discipline + Pattern C fragility detector | Pattern C deployable as fragility detector |
|
|
251
257
|
| 4 — Loki/CH-logs pipeline divergence | OTLP logs routing | manifest warnings + shape-aware diagnostic | Pattern A |
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# check-lgtm-gate.sh — Claude Code PreToolUse hook that blocks
|
|
4
|
+
# `gh pr merge` invocations when no non-author APPROVED review exists
|
|
5
|
+
# on the target PR. Implements `pr-discipline.md` §"How to submit
|
|
6
|
+
# LGTM — formal review, not comment" + §"When the reviewer is absent
|
|
7
|
+
# or unreachable" structurally.
|
|
8
|
+
#
|
|
9
|
+
# Hook contract: JSON on stdin, exit 0 = allow, exit 2 = block (stderr
|
|
10
|
+
# is fed back to Claude as the error). Same shape as #140's
|
|
11
|
+
# check-gh-token.sh + #244/#272's check-mention-routing.sh per
|
|
12
|
+
# groundnuty/macf#270 design alignment.
|
|
13
|
+
#
|
|
14
|
+
# Architectural note (DR-023 amendment 2026-04-27, PR #279): this is a
|
|
15
|
+
# bash command-type hook, NOT `type: "mcp_tool"`. PreToolUse-blocking
|
|
16
|
+
# semantics + mcp_tool's non-blocking-on-disconnect failure mode are
|
|
17
|
+
# structurally incompatible — bash form fires uniformly across substrate
|
|
18
|
+
# (no macf-agent MCP server) AND consumer workspaces (startup window,
|
|
19
|
+
# transient disconnect). Same reasoning UC-4 (PR #275) demonstrated.
|
|
20
|
+
#
|
|
21
|
+
# Override: MACF_SKIP_LGTM_CHECK=1 bypasses (for legitimate operator-
|
|
22
|
+
# allowed exceptions per pr-discipline.md §"When the reviewer is absent
|
|
23
|
+
# or unreachable" — reporter-sanctioned self-merge, urgent revert, etc.).
|
|
24
|
+
#
|
|
25
|
+
# Refs: groundnuty/macf#270 (this hook); pr-discipline.md (canonical
|
|
26
|
+
# rule, distributed via `macf rules refresh`); DR-023 amendment
|
|
27
|
+
# (bash-form decision rule); macf#262 / PR #263 (LGTM rule
|
|
28
|
+
# codification); PR #275 / macf#244+#272 (empirical pattern).
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
|
|
31
|
+
# Cheap exit on operator override — no stdin read, no parsing.
|
|
32
|
+
if [[ "${MACF_SKIP_LGTM_CHECK:-}" == "1" ]]; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Read PreToolUse payload. Fall through to allow on parse error — a
|
|
37
|
+
# broken hook must not brick the harness. Same defense-in-depth as
|
|
38
|
+
# check-gh-token.sh.
|
|
39
|
+
INPUT_JSON="$(cat 2>/dev/null || echo "")"
|
|
40
|
+
COMMAND="$(jq -r '.tool_input.command // ""' <<<"$INPUT_JSON" 2>/dev/null || echo "")"
|
|
41
|
+
|
|
42
|
+
if [[ -z "$COMMAND" ]]; then
|
|
43
|
+
# No command extractable — allow (defense-in-depth).
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Wrapper-aware match for `gh pr merge`. Mirrors check-mention-routing.sh's
|
|
48
|
+
# pattern shape — covers sudo, env VAR=, watch, ionice, setsid, nice,
|
|
49
|
+
# time prefix wrappers + chained-form leadins `;` `|` `&`. Subcommand
|
|
50
|
+
# match: `gh pr merge` ONLY (NOT `gh pr view`, `gh pr list`, etc.) —
|
|
51
|
+
# trailing whitespace or end-of-string forces exact-subcommand match.
|
|
52
|
+
GH_MERGE_PATTERN='(^|[[:space:];|&])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|watch[[:space:]]+|ionice[[:space:]]+|setsid[[:space:]]+|nice[[:space:]]+|time[[:space:]]+|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*gh[[:space:]]+pr[[:space:]]+merge([[:space:]]|$)'
|
|
53
|
+
|
|
54
|
+
# Shell-wrapper bypass: catches `bash -c "gh pr merge ..."` and variants.
|
|
55
|
+
# Same flag-handling logic as check-gh-token.sh + check-mention-routing.sh.
|
|
56
|
+
SHELL_C_GH_MERGE_PATTERN='(^|[[:space:];|&])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*(bash|sh|zsh)[[:space:]]+(-[a-zA-Z]+[[:space:]]+)*-[a-zA-Z]*c[[:space:]]+[^[:space:]].*gh[[:space:]]+pr[[:space:]]+merge([[:space:]]|$)'
|
|
57
|
+
|
|
58
|
+
if [[ ! "$COMMAND" =~ $GH_MERGE_PATTERN ]] && [[ ! "$COMMAND" =~ $SHELL_C_GH_MERGE_PATTERN ]]; then
|
|
59
|
+
# Not a `gh pr merge` command — allow.
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Extract PR number from the matched command. The PR number is the
|
|
64
|
+
# first non-flag positional argument after `gh pr merge`. Examples:
|
|
65
|
+
# gh pr merge 123 --squash
|
|
66
|
+
# gh pr merge --squash 123
|
|
67
|
+
# gh pr merge 123 --repo owner/repo --squash --delete-branch
|
|
68
|
+
# bash -c "gh pr merge 123 --squash"
|
|
69
|
+
# We scan the whole command for `gh pr merge` then walk forward to find
|
|
70
|
+
# the first bare integer token (not preceded by `=` so it's not a flag
|
|
71
|
+
# value like `--retries=3`).
|
|
72
|
+
PR_NUMBER=""
|
|
73
|
+
# Strip leading wrappers up to and including `gh pr merge`. Use sed
|
|
74
|
+
# with extended regex to find the gh-pr-merge prefix and remove it.
|
|
75
|
+
TAIL="$(sed -E 's/^.*gh[[:space:]]+pr[[:space:]]+merge[[:space:]]+//' <<<"$COMMAND" 2>/dev/null || echo "")"
|
|
76
|
+
if [[ -z "$TAIL" ]]; then
|
|
77
|
+
# No tail after `gh pr merge` — likely the operator is invoking with
|
|
78
|
+
# no args (interactive prompt). Allow; the command will fail at gh
|
|
79
|
+
# itself with a usage error, not silently merge anything.
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Find the first bare-integer token. Skip flags (`--foo`, `-x`) and
|
|
84
|
+
# their values (handled via `[[ $prev == --* ]]` lookback for non-`=`
|
|
85
|
+
# flag-value form). We only need the FIRST PR number — `gh pr merge`
|
|
86
|
+
# only takes one positional.
|
|
87
|
+
PREV_FLAG=""
|
|
88
|
+
# shellcheck disable=SC2086
|
|
89
|
+
for tok in $TAIL; do
|
|
90
|
+
# Flags introducing a value with a separate-arg form (e.g. `--repo X`)
|
|
91
|
+
# — set PREV_FLAG so the next token is consumed as a value.
|
|
92
|
+
if [[ -n "$PREV_FLAG" ]]; then
|
|
93
|
+
PREV_FLAG=""
|
|
94
|
+
continue
|
|
95
|
+
fi
|
|
96
|
+
# `--flag=value` form — single token, no lookahead needed.
|
|
97
|
+
if [[ "$tok" =~ ^--[a-zA-Z0-9-]+= ]]; then
|
|
98
|
+
continue
|
|
99
|
+
fi
|
|
100
|
+
# `--flag` without `=` — set PREV_FLAG to consume next token as value.
|
|
101
|
+
# Boolean flags (e.g. `--squash`, `--delete-branch`) don't consume a
|
|
102
|
+
# value, but we can't tell from the command alone — heuristic: the
|
|
103
|
+
# first non-flag token AFTER any flags is the PR number, even if we
|
|
104
|
+
# mistakenly skip a boolean's "value." If that "value" is the PR
|
|
105
|
+
# number itself, we miss it; mitigation: gh's canonical positional
|
|
106
|
+
# ordering is `gh pr merge <N> [flags]`, so PR-first is the common
|
|
107
|
+
# form. The few cases that put flags first AND use boolean flags
|
|
108
|
+
# without `=` are rare; fail-open is acceptable per defense-in-depth.
|
|
109
|
+
if [[ "$tok" =~ ^-- ]]; then
|
|
110
|
+
# Known value-taking flags from `gh pr merge --help`:
|
|
111
|
+
# --repo, --body, --body-file, --match-head-commit, --subject,
|
|
112
|
+
# --author-email
|
|
113
|
+
# Boolean flags don't consume a value:
|
|
114
|
+
# --squash, --merge, --rebase, --delete-branch, --auto, --admin,
|
|
115
|
+
# --disable-auto
|
|
116
|
+
case "$tok" in
|
|
117
|
+
--repo|--body|--body-file|--match-head-commit|--subject|--author-email)
|
|
118
|
+
PREV_FLAG="$tok"
|
|
119
|
+
;;
|
|
120
|
+
*) ;;
|
|
121
|
+
esac
|
|
122
|
+
continue
|
|
123
|
+
fi
|
|
124
|
+
# Short flag — `-X`. Same boolean-vs-value ambiguity. `gh pr merge`
|
|
125
|
+
# short flags from --help: `-s` (squash), `-m` (merge), `-r` (rebase),
|
|
126
|
+
# `-d` (delete-branch), `-R` (repo, value-taking), `-b` (body, value),
|
|
127
|
+
# `-F` (body-file, value), `-t` (subject, value).
|
|
128
|
+
if [[ "$tok" =~ ^-[a-zA-Z]$ ]]; then
|
|
129
|
+
case "$tok" in
|
|
130
|
+
-R|-b|-F|-t)
|
|
131
|
+
PREV_FLAG="$tok"
|
|
132
|
+
;;
|
|
133
|
+
*) ;;
|
|
134
|
+
esac
|
|
135
|
+
continue
|
|
136
|
+
fi
|
|
137
|
+
# Bare integer — this is our PR number.
|
|
138
|
+
if [[ "$tok" =~ ^[0-9]+$ ]]; then
|
|
139
|
+
PR_NUMBER="$tok"
|
|
140
|
+
break
|
|
141
|
+
fi
|
|
142
|
+
# URL form: `https://github.com/owner/repo/pull/<N>` — gh accepts
|
|
143
|
+
# this as a positional. Extract the trailing integer.
|
|
144
|
+
if [[ "$tok" =~ /pull/([0-9]+) ]]; then
|
|
145
|
+
PR_NUMBER="${BASH_REMATCH[1]}"
|
|
146
|
+
break
|
|
147
|
+
fi
|
|
148
|
+
# `owner/repo#N` shorthand — also accepted by gh.
|
|
149
|
+
if [[ "$tok" =~ \#([0-9]+) ]]; then
|
|
150
|
+
PR_NUMBER="${BASH_REMATCH[1]}"
|
|
151
|
+
break
|
|
152
|
+
fi
|
|
153
|
+
# Quoted forms — strip surrounding quotes before re-checking.
|
|
154
|
+
STRIPPED="${tok#\"}"
|
|
155
|
+
STRIPPED="${STRIPPED%\"}"
|
|
156
|
+
STRIPPED="${STRIPPED#\'}"
|
|
157
|
+
STRIPPED="${STRIPPED%\'}"
|
|
158
|
+
if [[ "$STRIPPED" =~ ^[0-9]+$ ]]; then
|
|
159
|
+
PR_NUMBER="$STRIPPED"
|
|
160
|
+
break
|
|
161
|
+
fi
|
|
162
|
+
done
|
|
163
|
+
|
|
164
|
+
if [[ -z "$PR_NUMBER" ]]; then
|
|
165
|
+
# Couldn't extract a PR number — allow per defense-in-depth.
|
|
166
|
+
# gh itself will fail with a usage error if the command is malformed,
|
|
167
|
+
# OR if it succeeds, the merge happens against the current branch's
|
|
168
|
+
# PR (gh auto-detects) — the LGTM gate is a structural guard, not a
|
|
169
|
+
# 100% catcher. Operator discipline + the canonical rule remain the
|
|
170
|
+
# primary defenses; the hook closes the residual.
|
|
171
|
+
exit 0
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
# Extract --repo if present so the api call targets the right repo.
|
|
175
|
+
# Without it, `gh api` falls back to the current git remote's repo,
|
|
176
|
+
# which works in most agent-shell flows but breaks for cross-repo merges.
|
|
177
|
+
REPO_FLAG=""
|
|
178
|
+
# Check for `--repo X` (separate-arg form)
|
|
179
|
+
if [[ "$COMMAND" =~ --repo[[:space:]]+([^[:space:]]+) ]]; then
|
|
180
|
+
REPO_FLAG="--repo ${BASH_REMATCH[1]}"
|
|
181
|
+
elif [[ "$COMMAND" =~ --repo=([^[:space:]]+) ]]; then
|
|
182
|
+
REPO_FLAG="--repo ${BASH_REMATCH[1]}"
|
|
183
|
+
elif [[ "$COMMAND" =~ (^|[[:space:]])-R[[:space:]]+([^[:space:]]+) ]]; then
|
|
184
|
+
REPO_FLAG="--repo ${BASH_REMATCH[2]}"
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
# Query PR author + reviews. Use `gh pr view --json author,reviews`
|
|
188
|
+
# rather than two separate `gh api` calls — single round-trip, gh
|
|
189
|
+
# handles repo detection if --repo was on the original command.
|
|
190
|
+
# Defense-in-depth: any failure (gh missing, network, 404, auth) →
|
|
191
|
+
# fail-open. Same posture as check-gh-token.sh.
|
|
192
|
+
#
|
|
193
|
+
# Strip surrounding quotes from REPO_FLAG's value if quoted.
|
|
194
|
+
PR_JSON=""
|
|
195
|
+
# shellcheck disable=SC2086
|
|
196
|
+
if ! PR_JSON="$(gh pr view "$PR_NUMBER" $REPO_FLAG --json author,reviews 2>/dev/null)"; then
|
|
197
|
+
exit 0
|
|
198
|
+
fi
|
|
199
|
+
if [[ -z "$PR_JSON" ]]; then
|
|
200
|
+
exit 0
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# author.login from `gh pr view --json author` returns either the bare
|
|
204
|
+
# login (e.g. "octocat") for users or `app/<name>` for bots (e.g.
|
|
205
|
+
# "app/macf-code-agent"). reviews[].author.login returns the bare
|
|
206
|
+
# login form (e.g. "octocat" or "macf-science-agent" — no `app/`
|
|
207
|
+
# prefix and no `[bot]` suffix). Normalize both to compare reliably:
|
|
208
|
+
# strip `app/` prefix from author and `[bot]` suffix from both sides.
|
|
209
|
+
PR_AUTHOR="$(jq -r '.author.login // ""' <<<"$PR_JSON" 2>/dev/null || echo "")"
|
|
210
|
+
PR_AUTHOR="${PR_AUTHOR#app/}"
|
|
211
|
+
PR_AUTHOR="${PR_AUTHOR%\[bot\]}"
|
|
212
|
+
|
|
213
|
+
if [[ -z "$PR_AUTHOR" ]]; then
|
|
214
|
+
# Couldn't parse author — fail-open.
|
|
215
|
+
exit 0
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
# Look for at least one APPROVED review where reviewer != author.
|
|
219
|
+
# `gh pr view --json reviews` returns `[{author: {login: "..."}, state: "APPROVED"}, ...]`.
|
|
220
|
+
NON_AUTHOR_APPROVALS="$(
|
|
221
|
+
jq -r --arg author "$PR_AUTHOR" '
|
|
222
|
+
[.reviews[]? |
|
|
223
|
+
select(.state == "APPROVED") |
|
|
224
|
+
(.author.login // "" | sub("^app/"; "") | sub("\\[bot\\]$"; "")) as $reviewer |
|
|
225
|
+
select($reviewer != "" and $reviewer != $author) |
|
|
226
|
+
$reviewer
|
|
227
|
+
] | length
|
|
228
|
+
' <<<"$PR_JSON" 2>/dev/null || echo "0"
|
|
229
|
+
)"
|
|
230
|
+
|
|
231
|
+
if [[ -z "$NON_AUTHOR_APPROVALS" ]] || ! [[ "$NON_AUTHOR_APPROVALS" =~ ^[0-9]+$ ]]; then
|
|
232
|
+
# Parse error — fail-open.
|
|
233
|
+
exit 0
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
if [[ "$NON_AUTHOR_APPROVALS" -ge 1 ]]; then
|
|
237
|
+
# At least one non-author APPROVED review — allow merge.
|
|
238
|
+
exit 0
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
# No non-author APPROVED review — block.
|
|
242
|
+
cat >&2 <<ERR
|
|
243
|
+
BLOCKED by MACF lgtm-gate hook: PR #${PR_NUMBER} has no non-author APPROVED
|
|
244
|
+
review on record. Per pr-discipline.md "no LGTM = no merge" (canonical rule
|
|
245
|
+
distributed via \`macf rules refresh\`):
|
|
246
|
+
|
|
247
|
+
"Without an explicit LGTM from the reviewer, the implementer does NOT
|
|
248
|
+
merge — even if waiting indefinitely."
|
|
249
|
+
|
|
250
|
+
PR author: ${PR_AUTHOR}
|
|
251
|
+
Non-author APPROVED reviews: 0
|
|
252
|
+
|
|
253
|
+
The LGTM gate is structural — it ensures someone other than the implementer
|
|
254
|
+
has read the diff in context. Self-merge without LGTM bypasses that quality
|
|
255
|
+
gate even if the work is correct.
|
|
256
|
+
|
|
257
|
+
Fix — request a formal review from a peer agent via state-change-firing
|
|
258
|
+
mechanism (NOT a plain comment, per pr-discipline.md §"How to submit LGTM"):
|
|
259
|
+
|
|
260
|
+
gh pr review ${PR_NUMBER} --approve --body "LGTM" # reviewer side
|
|
261
|
+
gh pr review ${PR_NUMBER} --request-changes --body "..." # changes needed
|
|
262
|
+
|
|
263
|
+
Then the reviewer @mentions you on the originating issue with the LGTM
|
|
264
|
+
state-change as the wake signal.
|
|
265
|
+
|
|
266
|
+
Override (ONLY for reporter-sanctioned exceptions per pr-discipline.md
|
|
267
|
+
§"When the reviewer is absent or unreachable"):
|
|
268
|
+
export MACF_SKIP_LGTM_CHECK=1
|
|
269
|
+
|
|
270
|
+
Refs: groundnuty/macf#270 (this hook); pr-discipline.md (canonical rule);
|
|
271
|
+
DR-023 amendment (bash-form decision rule); macf#262 / PR #263 (rule
|
|
272
|
+
codification origin).
|
|
273
|
+
ERR
|
|
274
|
+
exit 2
|