forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7
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/AGENTS.md +4 -0
- package/CHANGELOG.md +48 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +120 -9
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +78 -22
- package/lib/commands/worktree.js +155 -19
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/validation-receipt.js +190 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +16 -4
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/commitlint.js +13 -15
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +96 -15
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
package/lib/commands/gate.js
CHANGED
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
const path = require('node:path');
|
|
24
|
+
const { stripGlobalFlags } = require('../global-flags');
|
|
24
25
|
const { setConfigOverride } = require('../config-writer');
|
|
25
26
|
const { getDefaultRuntimeGraph, getResolvedRuntimeGraph } = require('../core/runtime-graph');
|
|
26
27
|
const {
|
|
27
28
|
recordGateEvent,
|
|
28
29
|
listGateEvents,
|
|
29
|
-
|
|
30
|
+
evaluateGateApproval,
|
|
30
31
|
} = require('../gate-events');
|
|
31
32
|
|
|
32
33
|
// The doc-update gate folds under this noun as `gate doc` (P2, kernel issue
|
|
@@ -38,14 +39,138 @@ const docGate = require('./doc-gate');
|
|
|
38
39
|
|
|
39
40
|
const TOGGLE_ACTIONS = new Set(['enable', 'disable']);
|
|
40
41
|
const EVENT_ACTIONS = new Set(['approve', 'reject', 'status', 'check']);
|
|
42
|
+
const MAX_GATE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
43
|
+
const GATE_JSON_SCHEMA = 'forge.gate.v1';
|
|
41
44
|
|
|
42
45
|
function usage() {
|
|
43
46
|
return [
|
|
44
|
-
'Usage: forge gate <enable|disable|approve|reject|status|check> [<issue-id>] <gate-id> [--reason <text>] [--json]',
|
|
47
|
+
'Usage: forge gate <enable|disable|approve|reject|status|check> [<issue-id>] <gate-id> [--ttl <duration>] [--reason <text>] [--json]',
|
|
45
48
|
' forge gate doc <detect|check|init|okf|...> [args] (doc-update gate; = forge doc-gate, run `forge doc-gate --help`)',
|
|
46
49
|
].join('\n');
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
function parseGateTtl(raw, issuedAt) {
|
|
53
|
+
if (raw === undefined) return { ok: true, expiresAt: null, requestIdentity: null };
|
|
54
|
+
const match = /^(\d+)(s|m|h|d)$/.exec(String(raw));
|
|
55
|
+
if (!match) return { ok: false, error: 'TTL must be a positive duration such as 30s, 5m, 2h, or 1d.' };
|
|
56
|
+
const multipliers = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
57
|
+
const ttlMs = Number(match[1]) * multipliers[match[2]];
|
|
58
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > MAX_GATE_TTL_MS) {
|
|
59
|
+
return { ok: false, error: 'TTL must be greater than zero and no more than 30d.' };
|
|
60
|
+
}
|
|
61
|
+
const issuedMillis = Date.parse(issuedAt);
|
|
62
|
+
if (!Number.isFinite(issuedMillis)) return { ok: false, error: 'Cannot issue a TTL approval without a valid clock.' };
|
|
63
|
+
const expiresMillis = issuedMillis + ttlMs;
|
|
64
|
+
if (!Number.isFinite(expiresMillis)) return { ok: false, error: 'TTL expiry is outside the supported date range.' };
|
|
65
|
+
const expiryDate = new Date(expiresMillis);
|
|
66
|
+
if (!Number.isFinite(expiryDate.getTime())) {
|
|
67
|
+
return { ok: false, error: 'TTL expiry is outside the supported date range.' };
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
expiresAt: expiryDate.toISOString(),
|
|
72
|
+
requestIdentity: `ttl-ms=${ttlMs}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseDecisionInput(action, rawArgs, flags) {
|
|
77
|
+
const issueId = rawArgs[0];
|
|
78
|
+
const gateId = rawArgs[1];
|
|
79
|
+
if (!issueId || !gateId || issueId.startsWith('--') || gateId.startsWith('--')) {
|
|
80
|
+
return { ok: false, error: `Missing issue id or gate id.\n${usage()}` };
|
|
81
|
+
}
|
|
82
|
+
if (flags.project !== undefined || flags['--project'] !== undefined) {
|
|
83
|
+
return { ok: false, error: 'Gate approvals are issue-scoped; --project is not supported.' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let ttl = flags.ttl ?? flags['--ttl'];
|
|
87
|
+
let reason = flags.reason ?? flags['--reason'];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (let index = 2; index < rawArgs.length; index += 1) {
|
|
90
|
+
const token = rawArgs[index];
|
|
91
|
+
if (token === '--json') {
|
|
92
|
+
if (seen.has('json')) return { ok: false, error: 'Duplicate --json flag.' };
|
|
93
|
+
seen.add('json');
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const match = /^(--ttl|--reason)(?:=(.*))?$/.exec(token);
|
|
97
|
+
if (!match) {
|
|
98
|
+
const kind = token.startsWith('--') ? 'Unknown flag' : 'Unexpected argument';
|
|
99
|
+
return { ok: false, error: `${kind} '${token}'.\n${usage()}` };
|
|
100
|
+
}
|
|
101
|
+
const name = match[1].slice(2);
|
|
102
|
+
if (seen.has(name)) return { ok: false, error: `Duplicate --${name} flag.` };
|
|
103
|
+
seen.add(name);
|
|
104
|
+
let value = match[2];
|
|
105
|
+
if (value === undefined) {
|
|
106
|
+
value = rawArgs[index + 1];
|
|
107
|
+
if (value === undefined || value.startsWith('--')) {
|
|
108
|
+
return { ok: false, error: `--${name} requires a value.` };
|
|
109
|
+
}
|
|
110
|
+
index += 1;
|
|
111
|
+
}
|
|
112
|
+
if (name === 'ttl') ttl = value;
|
|
113
|
+
if (name === 'reason') reason = value;
|
|
114
|
+
}
|
|
115
|
+
if (action !== 'approve' && ttl !== undefined) {
|
|
116
|
+
return { ok: false, error: '--ttl is valid only for gate approval.' };
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, issueId, gateId, ttl, reason };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseReadInput(action, rawArgs) {
|
|
122
|
+
const operandCount = action === 'status' ? 1 : 2;
|
|
123
|
+
const issueId = rawArgs[0];
|
|
124
|
+
const gateId = operandCount === 2 ? rawArgs[1] : undefined;
|
|
125
|
+
if (!issueId || issueId.startsWith('--') || (operandCount === 2 && (!gateId || gateId.startsWith('--')))) {
|
|
126
|
+
const missing = action === 'status' ? 'issue id' : 'issue id or gate id';
|
|
127
|
+
return { ok: false, error: `Missing ${missing}.\n${usage()}` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let jsonSeen = false;
|
|
131
|
+
for (const token of rawArgs.slice(operandCount)) {
|
|
132
|
+
if (token === '--json') {
|
|
133
|
+
if (jsonSeen) return { ok: false, error: 'Duplicate --json flag.' };
|
|
134
|
+
jsonSeen = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const kind = token.startsWith('--') ? 'Unknown flag' : 'Unexpected argument';
|
|
138
|
+
return { ok: false, error: `${kind} '${token}'.\n${usage()}` };
|
|
139
|
+
}
|
|
140
|
+
return { ok: true, issueId, gateId };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function gateJsonResult(action, result, context = {}) {
|
|
144
|
+
const event = result.event ?? null;
|
|
145
|
+
const envelope = {
|
|
146
|
+
schema_version: GATE_JSON_SCHEMA,
|
|
147
|
+
command: `gate.${action}`,
|
|
148
|
+
ok: result.success === true,
|
|
149
|
+
issue_id: event?.issue_id ?? result.issueId ?? context.issueId ?? null,
|
|
150
|
+
};
|
|
151
|
+
const gateId = event?.gate ?? context.gateId;
|
|
152
|
+
if (action !== 'status' && gateId !== undefined && gateId !== null) {
|
|
153
|
+
envelope.gate_id = gateId;
|
|
154
|
+
}
|
|
155
|
+
if (action === 'approve' || action === 'reject') {
|
|
156
|
+
envelope.duplicate = result.duplicate === true;
|
|
157
|
+
envelope.decision = event?.decision ?? (action === 'approve' ? 'approved' : 'rejected');
|
|
158
|
+
envelope.event = event;
|
|
159
|
+
}
|
|
160
|
+
if (action === 'status') {
|
|
161
|
+
envelope.issue = envelope.issue_id;
|
|
162
|
+
envelope.events = result.events ?? [];
|
|
163
|
+
}
|
|
164
|
+
if (action === 'check') {
|
|
165
|
+
envelope.approved = result.approved === true;
|
|
166
|
+
envelope.satisfied = result.success === true;
|
|
167
|
+
envelope.disabled = result.disabled === true;
|
|
168
|
+
envelope.event = event;
|
|
169
|
+
}
|
|
170
|
+
if (result.error) envelope.error = result.error;
|
|
171
|
+
return { ...result, output: JSON.stringify(envelope, null, 2) };
|
|
172
|
+
}
|
|
173
|
+
|
|
49
174
|
// The known-toggle set is gates PLUS unlocked toggleable rails (e.g.
|
|
50
175
|
// rail.kernel_tracking): both are governed through the same `forge gate
|
|
51
176
|
// enable|disable` surface and the resolver's rail-aware workflow.gates loop.
|
|
@@ -96,22 +221,27 @@ function handleToggle(action, gateId, projectRoot) {
|
|
|
96
221
|
};
|
|
97
222
|
}
|
|
98
223
|
|
|
99
|
-
async function handleDecision(action,
|
|
100
|
-
if (!
|
|
101
|
-
|
|
102
|
-
}
|
|
224
|
+
async function handleDecision(action, input, projectRoot, opts) {
|
|
225
|
+
if (!input.ok) return { success: false, error: input.error };
|
|
226
|
+
const { issueId, gateId } = input;
|
|
103
227
|
const known = validateKnownGate(gateId);
|
|
104
228
|
if (!known.ok) return { success: false, error: known.error };
|
|
105
229
|
|
|
106
230
|
const decision = action === 'approve' ? 'approved' : 'rejected';
|
|
107
|
-
const reason = typeof
|
|
231
|
+
const reason = typeof input.reason === 'string' ? input.reason : undefined;
|
|
232
|
+
const issuedAt = opts.now || new Date().toISOString();
|
|
233
|
+
const expiry = parseGateTtl(input.ttl, issuedAt);
|
|
234
|
+
if (!expiry.ok) return { success: false, error: expiry.error };
|
|
108
235
|
const result = await recordGateEvent(projectRoot, {
|
|
109
236
|
issueId,
|
|
110
237
|
gateId,
|
|
111
238
|
decision,
|
|
112
239
|
reason,
|
|
240
|
+
expiresAt: expiry.expiresAt,
|
|
241
|
+
requestIdentity: expiry.requestIdentity,
|
|
113
242
|
env: opts.env,
|
|
114
243
|
deps: kernelDeps(opts),
|
|
244
|
+
now: issuedAt,
|
|
115
245
|
});
|
|
116
246
|
|
|
117
247
|
if (result.issueMissing) {
|
|
@@ -125,29 +255,26 @@ async function handleDecision(action, issueId, gateId, flags, projectRoot, opts)
|
|
|
125
255
|
success: true,
|
|
126
256
|
duplicate: result.duplicate === true,
|
|
127
257
|
actor: result.actor,
|
|
128
|
-
|
|
258
|
+
event: result.event,
|
|
259
|
+
output: `${verb} gate '${gateId}' for ${issueId} (control ${result.event.control_id}; actor ${result.actor}; issued ${result.event.issued_at}; expires ${result.event.expires_at || 'never'})${reasonNote}${suffix}`,
|
|
129
260
|
};
|
|
130
261
|
}
|
|
131
262
|
|
|
132
|
-
async function handleStatus(issueId,
|
|
263
|
+
async function handleStatus(issueId, _flags, projectRoot, opts) {
|
|
133
264
|
if (!issueId) {
|
|
134
265
|
return { success: false, error: `Missing issue id.\n${usage()}` };
|
|
135
266
|
}
|
|
136
267
|
const events = await listGateEvents(projectRoot, issueId, { deps: kernelDeps(opts) });
|
|
137
268
|
|
|
138
|
-
if (flags.json) {
|
|
139
|
-
return { success: true, output: JSON.stringify({ issue: issueId, events }, null, 2) };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
269
|
if (events.length === 0) {
|
|
143
|
-
return { success: true, output: `No gate events for ${issueId}.` };
|
|
270
|
+
return { success: true, issueId, events, output: `No gate events for ${issueId}.` };
|
|
144
271
|
}
|
|
145
272
|
const lines = events.map(event => {
|
|
146
273
|
const label = event.event_type === 'gate.approved' ? 'APPROVED' : 'REJECTED';
|
|
147
274
|
const reason = event.reason ? ` — ${event.reason}` : '';
|
|
148
|
-
return `${label} ${event.gate} by ${event.actor} at ${event.
|
|
275
|
+
return `${label} ${event.gate} for ${event.issue_id} (${event.control_id}) by ${event.actor} at ${event.issued_at}; expires ${event.expires_at || 'never'}${reason}`;
|
|
149
276
|
});
|
|
150
|
-
return { success: true, output: `Gate events for ${issueId}:\n${lines.join('\n')}` };
|
|
277
|
+
return { success: true, issueId, events, output: `Gate events for ${issueId}:\n${lines.join('\n')}` };
|
|
151
278
|
}
|
|
152
279
|
|
|
153
280
|
async function handleCheck(issueId, gateId, projectRoot, opts) {
|
|
@@ -162,18 +289,42 @@ async function handleCheck(issueId, gateId, projectRoot, opts) {
|
|
|
162
289
|
const resolved = getResolvedRuntimeGraph({ projectRoot });
|
|
163
290
|
const resolvedGate = [...resolved.gates, ...resolved.rails].find(gate => gate.id === gateId);
|
|
164
291
|
if (resolvedGate && resolvedGate.enabled === false) {
|
|
165
|
-
return {
|
|
292
|
+
return {
|
|
293
|
+
success: true,
|
|
294
|
+
issueId,
|
|
295
|
+
approved: false,
|
|
296
|
+
disabled: true,
|
|
297
|
+
output: `gate ${gateId} is disabled — satisfied for ${issueId}`,
|
|
298
|
+
};
|
|
166
299
|
}
|
|
167
300
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
301
|
+
const events = await listGateEvents(projectRoot, issueId, { deps: kernelDeps(opts) });
|
|
302
|
+
const decision = evaluateGateApproval(events, gateId, opts.now);
|
|
303
|
+
const event = decision.event;
|
|
304
|
+
if (decision.approved) {
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
event,
|
|
308
|
+
approved: true,
|
|
309
|
+
output: `gate ${gateId} approved for ${issueId} (control ${event.control_id}; actor ${event.actor}; decision ${event.decision}; issued ${event.issued_at}; expires ${event.expires_at || 'never'})`,
|
|
310
|
+
};
|
|
171
311
|
}
|
|
172
|
-
|
|
312
|
+
const evidence = event
|
|
313
|
+
? ` (control ${event.control_id}; actor ${event.actor}; decision ${event.decision}; issued ${event.issued_at}; expires ${event.expires_at || 'never'})`
|
|
314
|
+
: '';
|
|
315
|
+
return {
|
|
316
|
+
success: false,
|
|
317
|
+
event,
|
|
318
|
+
issueId,
|
|
319
|
+
approved: false,
|
|
320
|
+
error: `gate ${gateId} not approved for ${issueId}${evidence}`,
|
|
321
|
+
};
|
|
173
322
|
}
|
|
174
323
|
|
|
175
324
|
async function handler(args, flags = {}, projectRoot = process.cwd(), opts = {}) {
|
|
176
|
-
const
|
|
325
|
+
const normalizedArgs = stripGlobalFlags(args);
|
|
326
|
+
const [action, ...rest] = normalizedArgs;
|
|
327
|
+
const wantsJson = flags.json === true || flags['--json'] === true || normalizedArgs.includes('--json');
|
|
177
328
|
|
|
178
329
|
// `gate doc [<doc-gate sub> ...]` → the standalone doc-gate handler, with the
|
|
179
330
|
// consumed `doc` token dropped so its own arg shape (detect/check/init/okf …)
|
|
@@ -186,13 +337,28 @@ async function handler(args, flags = {}, projectRoot = process.cwd(), opts = {})
|
|
|
186
337
|
return handleToggle(action, rest[0], projectRoot);
|
|
187
338
|
}
|
|
188
339
|
if (EVENT_ACTIONS.has(action)) {
|
|
340
|
+
let result;
|
|
341
|
+
let input;
|
|
189
342
|
if (action === 'status') {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
343
|
+
input = parseReadInput(action, rest);
|
|
344
|
+
result = input.ok
|
|
345
|
+
? await handleStatus(input.issueId, flags, projectRoot, opts)
|
|
346
|
+
: { success: false, issueId: rest[0], error: input.error };
|
|
347
|
+
} else if (action === 'check') {
|
|
348
|
+
input = parseReadInput(action, rest);
|
|
349
|
+
result = input.ok
|
|
350
|
+
? await handleCheck(input.issueId, input.gateId, projectRoot, opts)
|
|
351
|
+
: { success: false, issueId: rest[0], approved: false, error: input.error };
|
|
352
|
+
} else {
|
|
353
|
+
input = parseDecisionInput(action, rest, flags);
|
|
354
|
+
result = await handleDecision(action, input, projectRoot, opts);
|
|
194
355
|
}
|
|
195
|
-
|
|
356
|
+
const context = input.ok
|
|
357
|
+
? { issueId: input.issueId, ...(input.gateId ? { gateId: input.gateId } : {}) }
|
|
358
|
+
: { issueId: rest[0] };
|
|
359
|
+
return wantsJson
|
|
360
|
+
? gateJsonResult(action, result, context)
|
|
361
|
+
: result;
|
|
196
362
|
}
|
|
197
363
|
|
|
198
364
|
return {
|
|
@@ -206,4 +372,8 @@ module.exports = {
|
|
|
206
372
|
description: 'Toggle a workflow gate, or record/query human-gate approval events',
|
|
207
373
|
usage: usage(),
|
|
208
374
|
handler,
|
|
375
|
+
parseGateTtl,
|
|
376
|
+
parseDecisionInput,
|
|
377
|
+
parseReadInput,
|
|
378
|
+
gateJsonResult,
|
|
209
379
|
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
const { constants } = require('node:os');
|
|
5
|
+
const spawn = require('cross-spawn');
|
|
6
|
+
const {
|
|
7
|
+
createGithubContext, prepareGithubAccount, readGithubAccount,
|
|
8
|
+
writeGithubAccount, unsetGithubAccount, validateGithubLogin,
|
|
9
|
+
} = require('../github-context');
|
|
10
|
+
const { stripGlobalFlags } = require('../global-flags');
|
|
11
|
+
const { defaultResolveLocalRepository } = require('./merge');
|
|
12
|
+
|
|
13
|
+
const usage = 'forge github use <login> | status [--json] | unset | run -- <program> [args...]';
|
|
14
|
+
const ERROR_MESSAGES = {
|
|
15
|
+
INVALID_GITHUB_LOGIN: 'GitHub login must be 1-39 letters, numbers, or internal hyphens.',
|
|
16
|
+
GIT_CONFIG_FAILED: 'Unable to access this clone-local GitHub account binding.',
|
|
17
|
+
GITHUB_NOT_AUTHENTICATED: 'Account is not authenticated. Run gh auth login --hostname github.com for the account, then retry.',
|
|
18
|
+
GITHUB_AUTH_FAILED: 'Unable to verify the account. Run gh auth login --hostname github.com, then retry.',
|
|
19
|
+
GITHUB_CLI_UNSUPPORTED: 'Upgrade GitHub CLI to support gh auth token --hostname github.com --user <login>.',
|
|
20
|
+
GITHUB_ACCOUNT_MISMATCH: 'The authenticated GitHub account does not match this clone binding.',
|
|
21
|
+
GITHUB_COMMAND_FAILED: 'The selected account cannot view this repository. Check repository permissions and organization SSO.',
|
|
22
|
+
GITHUB_REPOSITORY_INVALID: 'Origin must identify a GitHub.com repository. Check its URL and SSH alias configuration.',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function safeFailure(error) {
|
|
26
|
+
const code = Object.hasOwn(ERROR_MESSAGES, error?.code) ? error.code : 'GITHUB_OPERATION_FAILED';
|
|
27
|
+
return { success: false, code, error: ERROR_MESSAGES[code] || 'GitHub account operation failed. Run forge github status for diagnostics.' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isGithubOrigin(remote, root, options) {
|
|
31
|
+
try {
|
|
32
|
+
const scpHost = /^[A-Za-z]:/.test(remote) || /^(?:git|https?|ssh|file):/i.test(remote) ? null
|
|
33
|
+
: /^(?:[A-Za-z0-9._-]+@)?([A-Za-z0-9][A-Za-z0-9._-]*):[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.exec(remote);
|
|
34
|
+
const parsed = scpHost ? null : new URL(remote);
|
|
35
|
+
if (parsed && !['https:', 'ssh:'].includes(parsed.protocol)) return false;
|
|
36
|
+
const host = scpHost ? scpHost[1] : parsed.hostname;
|
|
37
|
+
if (host.toLowerCase() === 'github.com') return true;
|
|
38
|
+
if ((!scpHost && parsed.protocol !== 'ssh:') || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(host)) return false;
|
|
39
|
+
// Resolve configured SSH aliases locally; -G does not open a connection.
|
|
40
|
+
const run = options.runner || execFileSync;
|
|
41
|
+
const config = String(run('ssh', ['-G', host], {
|
|
42
|
+
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, timeout: 3000,
|
|
43
|
+
}));
|
|
44
|
+
return /^hostname\s+github\.com\s*$/im.test(config);
|
|
45
|
+
} catch { return false; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function repositoryAccess(context, root, options) {
|
|
49
|
+
const remote = readGit(root, ['remote', 'get-url', 'origin'], options);
|
|
50
|
+
const repository = defaultResolveLocalRepository({ projectRoot: root, git: () => remote });
|
|
51
|
+
if (!repository || !isGithubOrigin(remote, root, options)) {
|
|
52
|
+
throw Object.assign(new Error(ERROR_MESSAGES.GITHUB_REPOSITORY_INVALID), { code: 'GITHUB_REPOSITORY_INVALID' });
|
|
53
|
+
}
|
|
54
|
+
context.runGh(['repo', 'view', `github.com/${repository}`, '--json', 'nameWithOwner'], { cwd: root, timeout: 30000 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readGit(root, args, options) {
|
|
58
|
+
try {
|
|
59
|
+
const run = options.runner || execFileSync;
|
|
60
|
+
return String(run('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, timeout: 3000 }) || '').trim();
|
|
61
|
+
} catch { return ''; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function boundedText(value) {
|
|
65
|
+
// Author fields are display data, never terminal control sequences.
|
|
66
|
+
return value.replace(/\p{Cc}/gu, '').slice(0, 160) || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function transportOf(remote) {
|
|
70
|
+
if (!remote) return 'missing';
|
|
71
|
+
if (/^https:\/\//i.test(remote)) return 'https';
|
|
72
|
+
if (/^http:\/\//i.test(remote)) return 'http';
|
|
73
|
+
if (/^ssh:\/\//i.test(remote)
|
|
74
|
+
|| (!/^[A-Za-z]:/.test(remote) && !/^(?:git|https?|ssh|file):/i.test(remote)
|
|
75
|
+
&& /^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9][A-Za-z0-9._-]*:[^\\/\s][^\s]*$/.test(remote))) return 'ssh';
|
|
76
|
+
return 'other';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function helperOf(helper) {
|
|
80
|
+
if (!helper) return 'none';
|
|
81
|
+
if (/\bgh(?:\.exe)?\s+auth\s+git-credential\b/i.test(helper)) return 'github-cli';
|
|
82
|
+
if (/\bmanager(?:-core)?\b/i.test(helper)) return 'credential-manager';
|
|
83
|
+
return 'other';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function localDiagnostics(root, options) {
|
|
87
|
+
return {
|
|
88
|
+
author: {
|
|
89
|
+
name: boundedText(readGit(root, ['config', '--get', 'user.name'], options)),
|
|
90
|
+
email: boundedText(readGit(root, ['config', '--get', 'user.email'], options)),
|
|
91
|
+
},
|
|
92
|
+
transport: transportOf(readGit(root, ['remote', 'get-url', 'origin'], options)),
|
|
93
|
+
credentialHelper: helperOf(readGit(root, ['config', '--get-all', 'credential.helper'], options)),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderStatus(status) {
|
|
98
|
+
const lines = [
|
|
99
|
+
`GitHub: ${status.state}`,
|
|
100
|
+
`Clone account: ${status.account || 'not selected'}`,
|
|
101
|
+
`Verified login: ${status.login || 'unavailable'}`,
|
|
102
|
+
`Commit author: ${status.author.name || 'unset'} <${status.author.email || 'unset'}>`,
|
|
103
|
+
`Origin transport: ${status.transport}; credential helper: ${status.credentialHelper}`,
|
|
104
|
+
];
|
|
105
|
+
if (status.error) lines.push(status.error);
|
|
106
|
+
if (status.state === 'unbound') lines.push('Optional: authenticate with gh auth login, then select an account with forge github use <login>.');
|
|
107
|
+
if (status.transport === 'ssh') lines.push('SSH key selection is controlled by your SSH configuration.');
|
|
108
|
+
if (status.transport === 'https' && status.credentialHelper === 'github-cli') {
|
|
109
|
+
lines.push('HTTPS uses the GitHub CLI helper; a launched session may also use its selected token for Git transport.');
|
|
110
|
+
} else if (status.transport === 'https') {
|
|
111
|
+
lines.push('HTTPS Git credentials are selected separately by your credential helper.');
|
|
112
|
+
}
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function accountStatus(root, options) {
|
|
117
|
+
const status = { state: 'unbound', account: null, source: null, login: null, repositoryAccess: null, ...localDiagnostics(root, options) };
|
|
118
|
+
try {
|
|
119
|
+
const account = readGithubAccount(root, options);
|
|
120
|
+
if (!account) return status;
|
|
121
|
+
status.source = 'clone-local';
|
|
122
|
+
status.account = validateGithubLogin(account) ? account : null;
|
|
123
|
+
const context = prepareGithubAccount(root, account, options);
|
|
124
|
+
status.login = context.login;
|
|
125
|
+
status.repositoryAccess = false;
|
|
126
|
+
repositoryAccess(context, root, options);
|
|
127
|
+
status.repositoryAccess = true;
|
|
128
|
+
status.state = 'ready';
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const failure = safeFailure(error);
|
|
131
|
+
status.state = status.login ? 'no_repository_access' : 'unauthenticated';
|
|
132
|
+
if (failure.code === 'GITHUB_ACCOUNT_MISMATCH') {
|
|
133
|
+
status.state = 'mismatch';
|
|
134
|
+
const match = /^GitHub account mismatch: expected [A-Za-z0-9-]+, received ([A-Za-z0-9-]+)\.$/.exec(error.message);
|
|
135
|
+
status.login = match && validateGithubLogin(match[1]) ? match[1] : null;
|
|
136
|
+
}
|
|
137
|
+
status.code = failure.code;
|
|
138
|
+
status.error = failure.error;
|
|
139
|
+
}
|
|
140
|
+
return status;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function launchFailure(error) {
|
|
144
|
+
if (error?.code === 'ENOENT') return { success: false, code: 'ENOENT', exitCode: 127, error: 'Child program was not found. Check its installation and PATH.' };
|
|
145
|
+
if (error?.code === 'EACCES') return { success: false, code: 'EACCES', exitCode: 126, error: 'Child program is not executable.' };
|
|
146
|
+
return { success: false, code: 'CHILD_LAUNCH_FAILED', exitCode: 1, error: 'Unable to launch the child program.' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function launch(context, program, args, root, options) {
|
|
150
|
+
return new Promise(resolve => {
|
|
151
|
+
let child;
|
|
152
|
+
try {
|
|
153
|
+
child = context.runChild(program, args, { cwd: root, stdio: 'inherit', shell: false });
|
|
154
|
+
} catch (error) { resolve(launchFailure(error)); return; }
|
|
155
|
+
const signals = options.signalSource || process;
|
|
156
|
+
const forwardInt = () => child.kill('SIGINT');
|
|
157
|
+
const forwardTerm = () => child.kill('SIGTERM');
|
|
158
|
+
signals.on('SIGINT', forwardInt);
|
|
159
|
+
signals.on('SIGTERM', forwardTerm);
|
|
160
|
+
const finish = result => {
|
|
161
|
+
signals.removeListener('SIGINT', forwardInt);
|
|
162
|
+
signals.removeListener('SIGTERM', forwardTerm);
|
|
163
|
+
resolve(result);
|
|
164
|
+
};
|
|
165
|
+
child.once('error', error => finish(launchFailure(error)));
|
|
166
|
+
child.once('close', (code, signal) => {
|
|
167
|
+
const exitCode = Number.isInteger(code) ? code : 128 + (constants.signals[signal] || 1);
|
|
168
|
+
finish({ success: exitCode === 0, exitCode, signal, ...(exitCode ? { error: `Child program exited with status ${exitCode}.` } : {}) });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function githubStatusResult(projectRoot, flags, args, options) {
|
|
174
|
+
const status = accountStatus(projectRoot, options);
|
|
175
|
+
const json = flags.json || flags['--json'] || args.includes('--json');
|
|
176
|
+
return { success: ['ready', 'unbound'].includes(status.state), status,
|
|
177
|
+
output: json ? JSON.stringify(status, null, 2) : renderStatus(status),
|
|
178
|
+
...(status.error ? { error: status.error } : {}) };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function handler(args = [], flags = {}, projectRoot = process.cwd(), options = {}) {
|
|
182
|
+
const delimiter = args.indexOf('--');
|
|
183
|
+
const ownArgs = stripGlobalFlags(delimiter < 0 ? args : args.slice(0, delimiter));
|
|
184
|
+
const verb = ownArgs[0];
|
|
185
|
+
if (!verb || verb === 'help' || (delimiter < 0 && (args.includes('--help') || args.includes('-h')))) {
|
|
186
|
+
return { success: true, output: usage };
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
if (verb === 'use' && ownArgs.length === 2 && delimiter < 0) {
|
|
190
|
+
const context = prepareGithubAccount(projectRoot, ownArgs[1], options);
|
|
191
|
+
repositoryAccess(context, projectRoot, options);
|
|
192
|
+
writeGithubAccount(projectRoot, ownArgs[1], options);
|
|
193
|
+
return { success: true, account: context.account, output: `This clone uses GitHub account ${context.account}.` };
|
|
194
|
+
}
|
|
195
|
+
if (verb === 'status' && ownArgs.slice(1).every(arg => arg === '--json') && delimiter < 0) {
|
|
196
|
+
return githubStatusResult(projectRoot, flags, args, options);
|
|
197
|
+
}
|
|
198
|
+
if (verb === 'unset' && ownArgs.length === 1 && delimiter < 0) {
|
|
199
|
+
unsetGithubAccount(projectRoot, options);
|
|
200
|
+
return { success: true, output: 'Clone GitHub account binding removed; native account selection applies.' };
|
|
201
|
+
}
|
|
202
|
+
if (verb === 'run' && ownArgs.length === 1 && delimiter >= 0 && args[delimiter + 1]) {
|
|
203
|
+
const context = createGithubContext(projectRoot, { ...options, childRunner: options.childRunner || spawn });
|
|
204
|
+
return await launch(context, args[delimiter + 1], args.slice(delimiter + 2), projectRoot, options);
|
|
205
|
+
}
|
|
206
|
+
return { success: false, error: `Usage: ${usage}` };
|
|
207
|
+
} catch (error) { return safeFailure(error); }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
name: 'github',
|
|
212
|
+
description: 'Optional clone-local GitHub account selection and isolated session launch',
|
|
213
|
+
usage,
|
|
214
|
+
handler,
|
|
215
|
+
};
|
package/lib/commands/hooks.js
CHANGED
|
@@ -29,8 +29,8 @@ const {
|
|
|
29
29
|
} = require('../hook-global-installer');
|
|
30
30
|
const fs = require('node:fs');
|
|
31
31
|
const path = require('node:path');
|
|
32
|
-
const { sessionStartCapability, userPromptSubmitCapability, sessionEndCapability } = require('../hook-renderer');
|
|
33
|
-
const { buildMemoryDigest, defaultFetchIssues, defaultFetchNotes } = require('../memory-digest');
|
|
32
|
+
const { sessionStartCapability, userPromptSubmitCapability, sessionEndCapability, readAttentionCapability } = require('../hook-renderer');
|
|
33
|
+
const { buildMemoryDigest, buildReadAttentionDigest, defaultFetchIssues, defaultFetchNotes } = require('../memory-digest');
|
|
34
34
|
const { collectInbox, buildInboxNudge } = require('../inbox');
|
|
35
35
|
const { collectDigest } = require('../pr-monitor/digest');
|
|
36
36
|
const { loadDispatchText } = require('../using-forge');
|
|
@@ -172,6 +172,7 @@ function usage() {
|
|
|
172
172
|
+ ' forge hooks inbox-pickup --harness <claude> (machine-facing; emits UserPromptSubmit context)\n'
|
|
173
173
|
+ ' forge hooks shepherd-events --harness <claude> (machine-facing; emits UserPromptSubmit PR-monitor deltas)\n'
|
|
174
174
|
+ ' forge hooks memory-recall --harness <claude> (machine-facing; emits UserPromptSubmit query-relevant memory)\n'
|
|
175
|
+
+ ' forge hooks read-attention --harness <claude> (machine-facing; emits path-matched PreToolUse:Read context)\n'
|
|
175
176
|
+ ' forge hooks capture --harness <claude> --trigger <precompact|stop> (machine-facing; captures a session summary on exit)';
|
|
176
177
|
}
|
|
177
178
|
|
|
@@ -212,12 +213,23 @@ const CAPTURE_TAGS = ['type:session-summary', CAPTURE_AUTO_TAG];
|
|
|
212
213
|
* @returns {string}
|
|
213
214
|
*/
|
|
214
215
|
function buildCaptureNote(trigger, issues) {
|
|
215
|
-
const
|
|
216
|
-
|
|
216
|
+
const stableIssues = [...new Map(issues.map(issue => {
|
|
217
|
+
const id = String((issue && issue.id) || '');
|
|
217
218
|
const title = String((issue && (issue.title || issue.id)) || 'untitled').replace(/\s+/g, ' ').trim();
|
|
219
|
+
return [`${id}\0${title}`, { id, title }];
|
|
220
|
+
})).values()].sort((left, right) => {
|
|
221
|
+
const leftKey = `${left.id}\0${left.title}`;
|
|
222
|
+
const rightKey = `${right.id}\0${right.title}`;
|
|
223
|
+
if (leftKey < rightKey) return -1;
|
|
224
|
+
if (leftKey > rightKey) return 1;
|
|
225
|
+
return 0;
|
|
226
|
+
});
|
|
227
|
+
const capped = stableIssues.slice(0, CAPTURE_ISSUE_CAP);
|
|
228
|
+
const lines = capped.map(issue => {
|
|
229
|
+
const title = issue.title;
|
|
218
230
|
return `- ${title.length > CAPTURE_TITLE_CAP ? `${title.slice(0, CAPTURE_TITLE_CAP)}…` : title}`;
|
|
219
231
|
});
|
|
220
|
-
const more =
|
|
232
|
+
const more = stableIssues.length > capped.length ? `\n- …and ${stableIssues.length - capped.length} more` : '';
|
|
221
233
|
const body = lines.length
|
|
222
234
|
? `Session boundary (${trigger}) — in-progress:\n${lines.join('\n')}${more}`
|
|
223
235
|
: `Session boundary (${trigger}) — no in-progress issues.`;
|
|
@@ -299,6 +311,41 @@ function formatUserPromptSubmit(harness, text) {
|
|
|
299
311
|
return '';
|
|
300
312
|
}
|
|
301
313
|
|
|
314
|
+
function formatPreToolUse(harness, text) {
|
|
315
|
+
if (harness !== 'claude') return '';
|
|
316
|
+
return JSON.stringify({
|
|
317
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: text },
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function fetchReadAttentionNotes(projectRoot) {
|
|
322
|
+
return projectMemory.recent(projectRoot, 100).map(entry => ({
|
|
323
|
+
note: typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value),
|
|
324
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
325
|
+
sourceAgent: entry.sourceAgent,
|
|
326
|
+
timestamp: entry.timestamp,
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function handleReadAttention(rest, projectRoot, opts = {}) {
|
|
331
|
+
try {
|
|
332
|
+
const harness = parseHarness(rest);
|
|
333
|
+
const capability = readAttentionCapability(harness);
|
|
334
|
+
if (!capability.rendered) return { success: true, output: '', reason: capability.reason };
|
|
335
|
+
const readInput = opts.readInput || readHookStdin;
|
|
336
|
+
const payload = JSON.parse(readInput() || '{}');
|
|
337
|
+
const targetPath = payload?.tool_input?.file_path || payload?.tool_input?.path;
|
|
338
|
+
if (!targetPath) return { success: true, output: '' };
|
|
339
|
+
const fetchNotes = opts.fetchNotes || fetchReadAttentionNotes;
|
|
340
|
+
const notes = await fetchNotes(projectRoot, { ...opts, noteLimit: 100 });
|
|
341
|
+
const digest = buildReadAttentionDigest(targetPath, notes, opts);
|
|
342
|
+
if (digest.empty) return { success: true, output: '' };
|
|
343
|
+
return { success: true, output: formatPreToolUse(harness, digest.text) };
|
|
344
|
+
} catch {
|
|
345
|
+
return { success: true, output: '' };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
302
349
|
/**
|
|
303
350
|
* `forge hooks inbox-pickup --harness <h>` — the COMPLIANT comment-back CONTEXT hook. On each
|
|
304
351
|
* prompt it emits harness-native UserPromptSubmit JSON carrying a COMPACT count+pointer nudge
|
|
@@ -700,11 +747,12 @@ async function handler(args, flags = {}, projectRoot, opts = {}) {
|
|
|
700
747
|
if (action === 'inbox-pickup') return handleInboxPickup(args.slice(1), projectRoot, opts);
|
|
701
748
|
if (action === 'shepherd-events') return handleShepherdEvents(args.slice(1), projectRoot, opts);
|
|
702
749
|
if (action === 'memory-recall') return handleMemoryRecall(args.slice(1), projectRoot, opts);
|
|
750
|
+
if (action === 'read-attention') return handleReadAttention(args.slice(1), projectRoot, opts);
|
|
703
751
|
if (action === 'capture') return handleCapture(args.slice(1), projectRoot, opts);
|
|
704
752
|
if (action === 'install') return handleInstall(args, flags, opts);
|
|
705
753
|
return {
|
|
706
754
|
success: false,
|
|
707
|
-
error: `forge hooks supports: install, session-start, inbox-pickup, shepherd-events, capture.\n${usage()}`,
|
|
755
|
+
error: `forge hooks supports: install, session-start, inbox-pickup, shepherd-events, memory-recall, read-attention, capture.\n${usage()}`,
|
|
708
756
|
};
|
|
709
757
|
}
|
|
710
758
|
|