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/forge-issues.js
CHANGED
|
@@ -7,6 +7,8 @@ const { createLocalBroker, buildLocalBrokerConfig } = require('./kernel/broker')
|
|
|
7
7
|
const { createBuiltinSQLiteDriver } = require('./kernel/sqlite-driver');
|
|
8
8
|
const { detectWorktree } = require('./detect-worktree');
|
|
9
9
|
const { DEFAULT_LEASE_TTL_MS } = require('./kernel/lease-enforcer');
|
|
10
|
+
const { ISSUE_TYPES } = require('./kernel/issue-command-contract');
|
|
11
|
+
const { loadRuntimeGraphConfig } = require('./core/runtime-graph');
|
|
10
12
|
|
|
11
13
|
const OPERATION_METHOD_ALIASES = {
|
|
12
14
|
'dep.add': 'depAdd',
|
|
@@ -199,6 +201,75 @@ function resolveLeaseTtlMs(env = {}) {
|
|
|
199
201
|
return ttl;
|
|
200
202
|
}
|
|
201
203
|
|
|
204
|
+
function readIssueContractValues(loaded, fail) {
|
|
205
|
+
const missing = Symbol('missing issue contract config');
|
|
206
|
+
const ownDataValue = (object, key, path) => {
|
|
207
|
+
if (!object || (typeof object !== 'object' && typeof object !== 'function')) return missing;
|
|
208
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
209
|
+
if (!descriptor) return missing;
|
|
210
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
211
|
+
fail(`${path} must be a data property`);
|
|
212
|
+
}
|
|
213
|
+
return descriptor.value;
|
|
214
|
+
};
|
|
215
|
+
const issues = ownDataValue(loaded?.config, 'issues', 'issues');
|
|
216
|
+
const readiness = issues === missing ? missing : ownDataValue(issues, 'readiness', 'issues.readiness');
|
|
217
|
+
const configured = readiness === missing
|
|
218
|
+
? missing
|
|
219
|
+
: ownDataValue(readiness, 'contracts', 'issues.readiness.contracts');
|
|
220
|
+
if (configured === missing || configured == null) return null;
|
|
221
|
+
if (typeof configured !== 'object'
|
|
222
|
+
|| Array.isArray(configured)
|
|
223
|
+
|| Object.getPrototypeOf(configured) !== Object.prototype) {
|
|
224
|
+
fail('issues.readiness.contracts must be a plain JSON object');
|
|
225
|
+
}
|
|
226
|
+
const allowedKeys = new Set(['enabled', 'workClasses']);
|
|
227
|
+
const ownKeys = Reflect.ownKeys(configured);
|
|
228
|
+
const unknownKeys = ownKeys.filter(key => typeof key !== 'string' || !allowedKeys.has(key));
|
|
229
|
+
if (unknownKeys.length > 0) {
|
|
230
|
+
fail(`unknown issues.readiness.contracts keys: ${unknownKeys.map(String).sort((left, right) => left.localeCompare(right)).join(', ')}`);
|
|
231
|
+
}
|
|
232
|
+
const values = Object.create(null);
|
|
233
|
+
for (const key of ownKeys) {
|
|
234
|
+
const descriptor = Object.getOwnPropertyDescriptor(configured, key);
|
|
235
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
236
|
+
fail(`issues.readiness.contracts.${key} must be a data property`);
|
|
237
|
+
}
|
|
238
|
+
values[key] = descriptor.value;
|
|
239
|
+
}
|
|
240
|
+
return values;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function resolveIssueContractPolicy(projectRoot, deps = {}) {
|
|
244
|
+
const loadConfig = deps.loadRuntimeGraphConfig || loadRuntimeGraphConfig;
|
|
245
|
+
const loaded = loadConfig({ projectRoot });
|
|
246
|
+
const fail = message => {
|
|
247
|
+
const error = new Error(`FORGE_ISSUE_CONTRACT_CONFIG_INVALID: ${message}`);
|
|
248
|
+
error.code = 'FORGE_ISSUE_CONTRACT_CONFIG_INVALID';
|
|
249
|
+
throw error;
|
|
250
|
+
};
|
|
251
|
+
if (Array.isArray(loaded?.errors) && loaded.errors.length > 0) {
|
|
252
|
+
fail(loaded.errors.map(error => error.message || error.code || String(error)).join('; '));
|
|
253
|
+
}
|
|
254
|
+
const values = readIssueContractValues(loaded, fail);
|
|
255
|
+
if (values === null) return null;
|
|
256
|
+
if (values.enabled !== undefined && typeof values.enabled !== 'boolean') {
|
|
257
|
+
fail('issues.readiness.contracts.enabled must be a boolean');
|
|
258
|
+
}
|
|
259
|
+
if (values.enabled !== true) return { enabled: false, workClasses: [] };
|
|
260
|
+
if (!Array.isArray(values.workClasses) || values.workClasses.length === 0) {
|
|
261
|
+
fail('issues.readiness.contracts.workClasses must be a non-empty array');
|
|
262
|
+
}
|
|
263
|
+
const allowedClasses = new Set(ISSUE_TYPES);
|
|
264
|
+
if (values.workClasses.some(value => typeof value !== 'string' || !allowedClasses.has(value))) {
|
|
265
|
+
fail('issues.readiness.contracts.workClasses contains an unknown work class');
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
enabled: true,
|
|
269
|
+
workClasses: [...new Set(values.workClasses)],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
202
273
|
async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
|
|
203
274
|
const injectedService = deps.createService;
|
|
204
275
|
|
|
@@ -206,6 +277,7 @@ async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
|
|
|
206
277
|
backend: createKernelIssueBackend({ projectRoot, deps }),
|
|
207
278
|
}));
|
|
208
279
|
|
|
280
|
+
const contractPolicy = resolveIssueContractPolicy(projectRoot, deps);
|
|
209
281
|
const service = createService();
|
|
210
282
|
// Thread a distinct per-agent actor (and, when present, a stable session id) into the
|
|
211
283
|
// mutation context so the Kernel's claim idempotency key is scoped per agent rather than
|
|
@@ -223,6 +295,9 @@ async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
|
|
|
223
295
|
const isClaim = operation === 'claim';
|
|
224
296
|
const worktreeId = isClaim ? resolveWorktreeId(projectRoot, actorEnv, deps) : undefined;
|
|
225
297
|
const leaseTtlMs = isClaim ? resolveLeaseTtlMs(actorEnv) : undefined;
|
|
298
|
+
const isTrustedAdoption = typeof deps.verifyIssueContractAdoption === 'function'
|
|
299
|
+
? deps.verifyIssueContractAdoption
|
|
300
|
+
: undefined;
|
|
226
301
|
try {
|
|
227
302
|
const result = await service.run(operation, rawArgs, {
|
|
228
303
|
projectRoot,
|
|
@@ -231,6 +306,8 @@ async function runIssueOperation(operation, rawArgs, projectRoot, deps = {}) {
|
|
|
231
306
|
...(sessionId ? { sessionId } : {}),
|
|
232
307
|
...(worktreeId ? { worktreeId } : {}),
|
|
233
308
|
...(leaseTtlMs ? { leaseTtlMs } : {}),
|
|
309
|
+
...(contractPolicy ? { contractPolicy } : {}),
|
|
310
|
+
...(isTrustedAdoption ? { isTrustedAdoption } : {}),
|
|
234
311
|
// Kernel mutations enqueue a projection-outbox "dirty marker" that `forge export`
|
|
235
312
|
// (the D16 git-JSONL portability projection) drains under target 'jsonl'. The
|
|
236
313
|
// broker's legacy primitive default is still 'beads', so without steering the target
|
|
@@ -267,4 +344,5 @@ module.exports = {
|
|
|
267
344
|
runIssueOperation,
|
|
268
345
|
isHelpInvocation,
|
|
269
346
|
resolveIssueActor,
|
|
347
|
+
resolveIssueContractPolicy,
|
|
270
348
|
};
|
package/lib/gate-events.js
CHANGED
|
@@ -49,8 +49,13 @@ const GATE_EVENT_ORIGIN = 'cli';
|
|
|
49
49
|
* SAME-actor re-approval mints no duplicate, while a later reject (different
|
|
50
50
|
* decision) or a different actor is recorded as its own event.
|
|
51
51
|
*/
|
|
52
|
-
function gateIdempotencyKey(eventType, issueId, gateId, actor) {
|
|
53
|
-
|
|
52
|
+
function gateIdempotencyKey(eventType, issueId, gateId, actor, expiresAt) {
|
|
53
|
+
const base = `${eventType}:${issueId}:${gateId}:${actor}`;
|
|
54
|
+
return expiresAt ? `${base}:${expiresAt}` : base;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function gateControlId(issueId, gateId) {
|
|
58
|
+
return `gate:${issueId}:${gateId}`;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
/** Shape a stored kernel_events row into the gate-event view callers consume. */
|
|
@@ -67,6 +72,18 @@ function parseGateEvent(row) {
|
|
|
67
72
|
actor: row.actor,
|
|
68
73
|
created_at: row.created_at,
|
|
69
74
|
};
|
|
75
|
+
// Older direct callers supplied only the legacy row subset and consumed its
|
|
76
|
+
// exact four-field view. Stored Kernel rows always carry entity_id, so enrich
|
|
77
|
+
// those audit/status views without changing that legacy parse contract.
|
|
78
|
+
if (row.entity_id) {
|
|
79
|
+
view.issue_id = row.entity_id;
|
|
80
|
+
view.control_id = payload.control_id || gateControlId(row.entity_id, payload.gate);
|
|
81
|
+
view.decision = row.event_type === GATE_APPROVED_EVENT ? 'approved' : 'rejected';
|
|
82
|
+
view.issued_at = row.created_at;
|
|
83
|
+
view.expires_at = payload.expires_at ?? null;
|
|
84
|
+
view.request_identity = payload.request_identity ?? null;
|
|
85
|
+
view.generation = Number.isInteger(payload.generation) ? payload.generation : 0;
|
|
86
|
+
}
|
|
70
87
|
if (payload.reason !== undefined) view.reason = payload.reason;
|
|
71
88
|
return view;
|
|
72
89
|
}
|
|
@@ -76,10 +93,43 @@ function isIdempotencyRace(error) {
|
|
|
76
93
|
return /UNIQUE constraint failed/i.test(message) && /idempotency_key/i.test(message);
|
|
77
94
|
}
|
|
78
95
|
|
|
96
|
+
function isActiveApproval(event, now) {
|
|
97
|
+
if (!event || event.event_type !== GATE_APPROVED_EVENT) return false;
|
|
98
|
+
if (!event.expires_at) return true;
|
|
99
|
+
const nowMillis = Date.parse(now);
|
|
100
|
+
const expiryMillis = Date.parse(event.expires_at);
|
|
101
|
+
return Number.isFinite(nowMillis) && Number.isFinite(expiryMillis) && nowMillis < expiryMillis;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function planGateGeneration(events, params) {
|
|
105
|
+
const { gateId, decision, actor, requestIdentity, now } = params;
|
|
106
|
+
const gateEvents = events.filter(event => event.gate === gateId);
|
|
107
|
+
const latest = gateEvents.at(-1);
|
|
108
|
+
const maxGeneration = gateEvents.reduce(
|
|
109
|
+
(max, event) => Math.max(max, Number.isInteger(event.generation) ? event.generation : 0),
|
|
110
|
+
-1,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (decision === 'approved') {
|
|
114
|
+
const identicalActive = latest
|
|
115
|
+
&& latest.actor === actor
|
|
116
|
+
&& latest.request_identity === requestIdentity
|
|
117
|
+
&& isActiveApproval(latest, now);
|
|
118
|
+
if (identicalActive) return { duplicateEvent: latest, generation: latest.generation };
|
|
119
|
+
return { duplicateEvent: null, generation: maxGeneration + 1 };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (latest?.event_type === GATE_REJECTED_EVENT && latest.actor === actor) {
|
|
123
|
+
return { duplicateEvent: latest, generation: latest.generation };
|
|
124
|
+
}
|
|
125
|
+
return { duplicateEvent: null, generation: Math.max(maxGeneration, 0) };
|
|
126
|
+
}
|
|
127
|
+
|
|
79
128
|
/**
|
|
80
|
-
* Record a gate decision as a kernel event on the issue.
|
|
81
|
-
*
|
|
82
|
-
*
|
|
129
|
+
* Record a gate decision as a kernel event on the issue. Active identical
|
|
130
|
+
* requests are idempotent; expiry/rejection advances the durable generation so
|
|
131
|
+
* a later approval can become authoritative. Validates the issue exists (the
|
|
132
|
+
* caller validates the gate id against the known-gate set before calling).
|
|
83
133
|
*
|
|
84
134
|
* @param {string} projectRoot
|
|
85
135
|
* @param {Object} params
|
|
@@ -93,7 +143,7 @@ function isIdempotencyRace(error) {
|
|
|
93
143
|
* @returns {Promise<{ ok: boolean, duplicate?: boolean, issueMissing?: boolean, event?: Object, actor?: string }>}
|
|
94
144
|
*/
|
|
95
145
|
async function recordGateEvent(projectRoot, params = {}) {
|
|
96
|
-
const { issueId, gateId, decision, reason, env, deps, now } = params;
|
|
146
|
+
const { issueId, gateId, decision, reason, expiresAt, requestIdentity, env, deps, now } = params;
|
|
97
147
|
const eventType = GATE_EVENT_TYPES[decision];
|
|
98
148
|
if (!eventType) {
|
|
99
149
|
throw new Error(`Unknown gate decision '${decision}' (expected 'approved' or 'rejected').`);
|
|
@@ -109,14 +159,42 @@ async function recordGateEvent(projectRoot, params = {}) {
|
|
|
109
159
|
return { ok: false, issueMissing: true, actor };
|
|
110
160
|
}
|
|
111
161
|
|
|
112
|
-
const
|
|
162
|
+
const createdAt = now || new Date().toISOString();
|
|
163
|
+
const stableRequestIdentity = requestIdentity ?? expiresAt ?? null;
|
|
164
|
+
const rows = await driver.listKernelEvents(ISSUE_ENTITY_TYPE, issueId, {}, config);
|
|
165
|
+
const generationPlan = planGateGeneration((rows || []).map(parseGateEvent), {
|
|
166
|
+
gateId,
|
|
167
|
+
decision,
|
|
168
|
+
actor,
|
|
169
|
+
requestIdentity: stableRequestIdentity,
|
|
170
|
+
now: createdAt,
|
|
171
|
+
});
|
|
172
|
+
if (generationPlan.duplicateEvent) {
|
|
173
|
+
return { ok: true, duplicate: true, event: generationPlan.duplicateEvent, actor };
|
|
174
|
+
}
|
|
175
|
+
const keyParts = [stableRequestIdentity];
|
|
176
|
+
if (generationPlan.generation > 0) keyParts.push(`generation=${generationPlan.generation}`);
|
|
177
|
+
const idempotencyKey = gateIdempotencyKey(
|
|
178
|
+
eventType,
|
|
179
|
+
issueId,
|
|
180
|
+
gateId,
|
|
181
|
+
actor,
|
|
182
|
+
keyParts.filter(Boolean).join(':') || undefined,
|
|
183
|
+
);
|
|
113
184
|
|
|
114
185
|
const existing = await driver.loadKernelEventByIdempotencyKey(idempotencyKey, {}, config);
|
|
115
186
|
if (existing) {
|
|
116
187
|
return { ok: true, duplicate: true, event: parseGateEvent(existing), actor };
|
|
117
188
|
}
|
|
118
189
|
|
|
119
|
-
const payload = {
|
|
190
|
+
const payload = {
|
|
191
|
+
gate: gateId,
|
|
192
|
+
actor,
|
|
193
|
+
control_id: gateControlId(issueId, gateId),
|
|
194
|
+
expires_at: expiresAt ?? null,
|
|
195
|
+
request_identity: stableRequestIdentity,
|
|
196
|
+
generation: generationPlan.generation,
|
|
197
|
+
};
|
|
120
198
|
if (typeof reason === 'string' && reason.length > 0) payload.reason = reason;
|
|
121
199
|
|
|
122
200
|
const event = {
|
|
@@ -128,7 +206,7 @@ async function recordGateEvent(projectRoot, params = {}) {
|
|
|
128
206
|
actor,
|
|
129
207
|
origin: GATE_EVENT_ORIGIN,
|
|
130
208
|
payload,
|
|
131
|
-
created_at:
|
|
209
|
+
created_at: createdAt,
|
|
132
210
|
};
|
|
133
211
|
|
|
134
212
|
try {
|
|
@@ -166,6 +244,12 @@ async function listGateEvents(projectRoot, issueId, options = {}) {
|
|
|
166
244
|
}
|
|
167
245
|
}
|
|
168
246
|
|
|
247
|
+
function evaluateGateApproval(events, gateId, now = new Date().toISOString()) {
|
|
248
|
+
const decisions = events.filter(event => event.gate === gateId);
|
|
249
|
+
const event = decisions.at(-1);
|
|
250
|
+
return { approved: isActiveApproval(event, now), event };
|
|
251
|
+
}
|
|
252
|
+
|
|
169
253
|
/**
|
|
170
254
|
* True iff a `gate.approved` event exists for this issue + gate.
|
|
171
255
|
*
|
|
@@ -177,7 +261,7 @@ async function listGateEvents(projectRoot, issueId, options = {}) {
|
|
|
177
261
|
*/
|
|
178
262
|
async function isGateApproved(projectRoot, issueId, gateId, options = {}) {
|
|
179
263
|
const events = await listGateEvents(projectRoot, issueId, options);
|
|
180
|
-
return events
|
|
264
|
+
return evaluateGateApproval(events, gateId, options.now).approved;
|
|
181
265
|
}
|
|
182
266
|
|
|
183
267
|
module.exports = {
|
|
@@ -185,8 +269,12 @@ module.exports = {
|
|
|
185
269
|
GATE_REJECTED_EVENT,
|
|
186
270
|
GATE_EVENT_TYPES,
|
|
187
271
|
gateIdempotencyKey,
|
|
272
|
+
gateControlId,
|
|
273
|
+
isActiveApproval,
|
|
274
|
+
planGateGeneration,
|
|
188
275
|
parseGateEvent,
|
|
189
276
|
recordGateEvent,
|
|
190
277
|
listGateEvents,
|
|
278
|
+
evaluateGateApproval,
|
|
191
279
|
isGateApproved,
|
|
192
280
|
};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const ACCOUNT_KEY = 'github.account';
|
|
6
|
+
const HOST = 'github.com';
|
|
7
|
+
const LOGIN_RE = /^(?=.{1,39}$)[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
|
|
8
|
+
const GITHUB_ENV_NAMES = new Set(['gh_token', 'github_token', 'gh_host', 'gh_repo']);
|
|
9
|
+
const GITHUB_SECRET_ENV_NAMES = new Set(['gh_token', 'github_token']);
|
|
10
|
+
|
|
11
|
+
class GithubContextError extends Error {
|
|
12
|
+
constructor(code, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'GithubContextError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
toJSON() {
|
|
19
|
+
return { code: this.code, message: this.message };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateGithubLogin(login) {
|
|
24
|
+
return typeof login === 'string' && LOGIN_RE.test(login);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeLogin(login) {
|
|
28
|
+
return validateGithubLogin(login) ? login : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runnerFor(options) {
|
|
32
|
+
return options.runner || options._runner || options._exec || options.execFileSync || defaultRunner;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function defaultRunner(command, args, options = {}) {
|
|
36
|
+
return execFileSync(command, args, {
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
...options,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function errorText(error) {
|
|
45
|
+
return [error?.message, error?.stderr?.toString?.(), error?.stdout?.toString?.()]
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(' ')
|
|
48
|
+
.toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isUnsupportedTokenCapability(error) {
|
|
52
|
+
const text = errorText(error);
|
|
53
|
+
return /unknown (?:flag|option)|invalid (?:flag|option)|unexpected argument|does not support/.test(text)
|
|
54
|
+
&& /user|hostname/.test(text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function safeCommandFailure(code, message) {
|
|
58
|
+
return new GithubContextError(code, message);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function safeGithubCommandFailure(error) {
|
|
62
|
+
const text = errorText(error);
|
|
63
|
+
const failure = safeCommandFailure(
|
|
64
|
+
'GITHUB_COMMAND_FAILED',
|
|
65
|
+
/rate limit|secondary rate/.test(text)
|
|
66
|
+
? 'The GitHub CLI command was rate limited.'
|
|
67
|
+
: 'The GitHub CLI command failed without exposing its output.',
|
|
68
|
+
);
|
|
69
|
+
const matchedStatus = /\bhttp\s+(401|403|429)\b/.exec(text)?.[1];
|
|
70
|
+
let httpStatus;
|
|
71
|
+
if ([401, 403, 429].includes(error?.httpStatus)) httpStatus = error.httpStatus;
|
|
72
|
+
else if ([401, 403, 429].includes(error?.status)) httpStatus = error.status;
|
|
73
|
+
else httpStatus = Number(matchedStatus) || undefined;
|
|
74
|
+
const retryAfter = Number(error?.retryAfter);
|
|
75
|
+
if (httpStatus) failure.httpStatus = httpStatus;
|
|
76
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) failure.retryAfter = retryAfter;
|
|
77
|
+
return failure;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function gitConfig(projectRoot, args, options, absentIsOkay = false) {
|
|
81
|
+
try {
|
|
82
|
+
return runnerFor(options)('git', ['config', '--local', ...args], {
|
|
83
|
+
cwd: projectRoot,
|
|
84
|
+
encoding: 'utf8',
|
|
85
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (absentIsOkay && (error?.status === 1 || /key does not contain|not found|no such key/i.test(errorText(error)))) {
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
throw safeCommandFailure('GIT_CONFIG_FAILED', 'Unable to read this clone-local GitHub account binding.');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readGithubAccount(projectRoot, options = {}) {
|
|
97
|
+
const output = gitConfig(projectRoot, ['--get', ACCOUNT_KEY], options, true);
|
|
98
|
+
const value = output == null ? '' : output.toString().trim();
|
|
99
|
+
return value || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function writeGithubAccount(projectRoot, login, options = {}) {
|
|
103
|
+
if (!validateGithubLogin(login)) {
|
|
104
|
+
throw safeCommandFailure('INVALID_GITHUB_LOGIN', 'GitHub login must be 1-39 letters, numbers, or internal hyphens.');
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
runnerFor(options)('git', ['config', '--local', '--replace-all', ACCOUNT_KEY, login], {
|
|
108
|
+
cwd: projectRoot,
|
|
109
|
+
encoding: 'utf8',
|
|
110
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
});
|
|
113
|
+
} catch (_error) {
|
|
114
|
+
throw safeCommandFailure('GIT_CONFIG_FAILED', 'Unable to write the clone-local GitHub account binding.');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function unsetGithubAccount(projectRoot, options = {}) {
|
|
119
|
+
try {
|
|
120
|
+
runnerFor(options)('git', ['config', '--local', '--unset-all', ACCOUNT_KEY], {
|
|
121
|
+
cwd: projectRoot,
|
|
122
|
+
encoding: 'utf8',
|
|
123
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
124
|
+
windowsHide: true,
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error?.status === 5 || /key does not contain|not found|no such key/i.test(errorText(error))) return;
|
|
128
|
+
throw safeCommandFailure('GIT_CONFIG_FAILED', 'Unable to remove the clone-local GitHub account binding.');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function withoutGithubEnvironment(environment) {
|
|
133
|
+
const result = {};
|
|
134
|
+
for (const [key, value] of Object.entries(environment || {})) {
|
|
135
|
+
if (!GITHUB_ENV_NAMES.has(key.toLowerCase())) result[key] = value;
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function selectedEnvironment(baseEnv, token, additions = {}) {
|
|
141
|
+
const result = withoutGithubEnvironment(baseEnv);
|
|
142
|
+
Object.assign(result, withoutGithubEnvironment(additions));
|
|
143
|
+
result.GH_TOKEN = token;
|
|
144
|
+
result.GITHUB_TOKEN = token;
|
|
145
|
+
result.GH_HOST = HOST;
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function passthroughEnvironment(baseEnv, additions = {}) {
|
|
150
|
+
return { ...baseEnv, ...additions };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function retrieveGithubToken(login, options) {
|
|
154
|
+
if (!validateGithubLogin(login)) {
|
|
155
|
+
throw safeCommandFailure('INVALID_GITHUB_LOGIN', 'GitHub login must be 1-39 letters, numbers, or internal hyphens.');
|
|
156
|
+
}
|
|
157
|
+
const baseEnv = options.baseEnv || options.env || process.env;
|
|
158
|
+
let output;
|
|
159
|
+
try {
|
|
160
|
+
output = runnerFor(options)('gh', ['auth', 'token', '--hostname', HOST, '--user', login], {
|
|
161
|
+
env: withoutGithubEnvironment(baseEnv),
|
|
162
|
+
encoding: 'utf8',
|
|
163
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
164
|
+
windowsHide: true,
|
|
165
|
+
});
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (isUnsupportedTokenCapability(error)) {
|
|
168
|
+
throw safeCommandFailure('GITHUB_CLI_UNSUPPORTED', 'Upgrade GitHub CLI, then authenticate the account with gh auth login.');
|
|
169
|
+
}
|
|
170
|
+
throw safeCommandFailure('GITHUB_NOT_AUTHENTICATED', 'GitHub account is not authenticated; run gh auth login for this account.');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const token = output == null ? '' : output.toString().trim();
|
|
174
|
+
if (!token) {
|
|
175
|
+
throw safeCommandFailure('GITHUB_NOT_AUTHENTICATED', 'GitHub account is not authenticated; run gh auth login for this account.');
|
|
176
|
+
}
|
|
177
|
+
return token;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function loginFromOutput(output) {
|
|
181
|
+
const text = output == null ? '' : output.toString().trim();
|
|
182
|
+
if (!text) return null;
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(text);
|
|
185
|
+
return safeLogin(parsed?.login);
|
|
186
|
+
} catch {
|
|
187
|
+
// `gh --jq` normally returns plain text; JSON is only a compatibility path.
|
|
188
|
+
return safeLogin(text.split(/\r?\n/, 1)[0].trim());
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveGithubLogin(token, baseEnv, options) {
|
|
193
|
+
try {
|
|
194
|
+
const output = runnerFor(options)('gh', ['api', '--hostname', HOST, 'user', '--jq', '.login'], {
|
|
195
|
+
env: selectedEnvironment(baseEnv, token),
|
|
196
|
+
encoding: 'utf8',
|
|
197
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
198
|
+
windowsHide: true,
|
|
199
|
+
});
|
|
200
|
+
const login = loginFromOutput(output);
|
|
201
|
+
if (!login) throw new Error('missing login');
|
|
202
|
+
return login;
|
|
203
|
+
} catch {
|
|
204
|
+
// Provider errors may contain response bodies or credentials; expose only a safe diagnosis.
|
|
205
|
+
throw safeCommandFailure('GITHUB_AUTH_FAILED', 'Unable to verify the selected GitHub account; run gh auth login and try again.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function prepareGithubAccount(_projectRoot, account, options = {}) {
|
|
210
|
+
if (!validateGithubLogin(account)) {
|
|
211
|
+
throw safeCommandFailure('INVALID_GITHUB_LOGIN', 'GitHub login must be 1-39 letters, numbers, or internal hyphens.');
|
|
212
|
+
}
|
|
213
|
+
const baseEnv = { ...(options.baseEnv || options.env || process.env) };
|
|
214
|
+
const token = retrieveGithubToken(account, { ...options, baseEnv });
|
|
215
|
+
const login = resolveGithubLogin(token, baseEnv, options);
|
|
216
|
+
if (login.toLowerCase() !== account.toLowerCase()) {
|
|
217
|
+
throw safeCommandFailure('GITHUB_ACCOUNT_MISMATCH', `GitHub account mismatch: expected ${account}, received ${login}.`);
|
|
218
|
+
}
|
|
219
|
+
return createPublicContext(publicStatus('ready', account, login), token, baseEnv, options);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function publicStatus(state, account, login) {
|
|
223
|
+
return Object.freeze({ state, account: account || null, login: login || null });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function createPublicContext(status, token, baseEnv, options) {
|
|
227
|
+
const context = {
|
|
228
|
+
status,
|
|
229
|
+
bound: status.state !== 'unbound',
|
|
230
|
+
account: status.account,
|
|
231
|
+
login: status.login,
|
|
232
|
+
};
|
|
233
|
+
const secrets = (status.state === 'unbound'
|
|
234
|
+
? Object.entries(baseEnv)
|
|
235
|
+
.filter(([key]) => GITHUB_SECRET_ENV_NAMES.has(key.toLowerCase()))
|
|
236
|
+
.map(([, value]) => value)
|
|
237
|
+
: [token])
|
|
238
|
+
.filter((value) => typeof value === 'string' && value);
|
|
239
|
+
const environmentForChild = (additions = {}) => status.state === 'unbound'
|
|
240
|
+
? passthroughEnvironment(baseEnv, additions)
|
|
241
|
+
: selectedEnvironment(baseEnv, token, additions);
|
|
242
|
+
const childRunner = options.childRunner || options._childRunner || runnerFor(options);
|
|
243
|
+
const runChild = (command, args, runOptions = {}) => {
|
|
244
|
+
if (typeof command !== 'string' || !Array.isArray(args) || args.some((arg) => typeof arg !== 'string')) {
|
|
245
|
+
throw safeCommandFailure('INVALID_GH_ARGUMENTS', 'GitHub CLI arguments must be an array of strings.');
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
return childRunner(command, args, {
|
|
249
|
+
...runOptions,
|
|
250
|
+
env: environmentForChild(runOptions.env),
|
|
251
|
+
});
|
|
252
|
+
} catch (_error) {
|
|
253
|
+
throw safeCommandFailure('GITHUB_COMMAND_FAILED', 'The GitHub CLI command failed without exposing its output.');
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const runGh = (args, runOptions = {}) => {
|
|
257
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== 'string')) {
|
|
258
|
+
throw safeCommandFailure('INVALID_GH_ARGUMENTS', 'GitHub CLI arguments must be an array of strings.');
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const output = runnerFor(options)('gh', args, {
|
|
262
|
+
...runOptions,
|
|
263
|
+
env: environmentForChild(runOptions.env),
|
|
264
|
+
encoding: 'utf8',
|
|
265
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
266
|
+
windowsHide: true,
|
|
267
|
+
});
|
|
268
|
+
return redactTokens(output, secrets);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
throw safeGithubCommandFailure(error);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
Object.defineProperties(context, {
|
|
274
|
+
runChild: { value: runChild, enumerable: false },
|
|
275
|
+
runGh: { value: runGh, enumerable: false },
|
|
276
|
+
getStatus: { value: () => ({ ...status }), enumerable: false },
|
|
277
|
+
});
|
|
278
|
+
return context;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function redactTokens(value, tokens) {
|
|
282
|
+
if (value == null) return value;
|
|
283
|
+
return tokens.slice().sort((left, right) => right.length - left.length)
|
|
284
|
+
.reduce((redacted, token) => redacted.split(token).join('[REDACTED]'), value.toString());
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function createGithubContext(projectRoot, options = {}) {
|
|
288
|
+
const baseEnv = { ...(options.baseEnv || options.env || process.env) };
|
|
289
|
+
const account = readGithubAccount(projectRoot, options);
|
|
290
|
+
if (!account) return createPublicContext(publicStatus('unbound', null, null), null, baseEnv, options);
|
|
291
|
+
if (!validateGithubLogin(account)) {
|
|
292
|
+
throw safeCommandFailure('INVALID_GITHUB_LOGIN', 'The clone-local github.account value is not a valid GitHub login.');
|
|
293
|
+
}
|
|
294
|
+
return prepareGithubAccount(projectRoot, account, { ...options, baseEnv });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
ACCOUNT_KEY,
|
|
299
|
+
GithubContextError,
|
|
300
|
+
createGithubContext,
|
|
301
|
+
prepareGithubAccount,
|
|
302
|
+
prepareGithubContext: createGithubContext,
|
|
303
|
+
readGithubAccount,
|
|
304
|
+
resolveGithubLogin,
|
|
305
|
+
unsetGithubAccount,
|
|
306
|
+
validateGithubLogin,
|
|
307
|
+
writeGithubAccount,
|
|
308
|
+
};
|