forge-workflow 0.1.0-beta.3 → 0.1.0-beta.5
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 +14 -7
- package/CHANGELOG.md +43 -1
- package/README.md +6 -2
- package/bin/forge-cmd.js +21 -1
- package/bin/forge.js +16 -369
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +9 -4
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +117 -17
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +397 -100
- package/lib/agents-config.js +5 -0
- package/lib/audit-evidence.js +71 -110
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/commands/_issue.js +31 -46
- package/lib/commands/_manifest.js +1 -1
- package/lib/commands/_registry.js +2 -2
- package/lib/commands/_resolve-command-opts.js +36 -29
- package/lib/commands/claim.js +2 -4
- package/lib/commands/clean.js +196 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/hooks.js +358 -13
- package/lib/commands/insights.js +8 -3
- package/lib/commands/merge.js +600 -40
- package/lib/commands/plan.js +23 -115
- package/lib/commands/pr.js +1 -1
- package/lib/commands/preflight.js +11 -2
- package/lib/commands/prime.js +23 -3
- package/lib/commands/push.js +41 -51
- package/lib/commands/recall.js +60 -16
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +18 -4
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +191 -95
- package/lib/commands/shepherd.js +49 -4
- package/lib/commands/ship.js +22 -23
- package/lib/commands/skill.js +383 -0
- package/lib/commands/status.js +54 -33
- package/lib/commands/test.js +56 -34
- package/lib/commands/worktree.js +247 -43
- package/lib/core/runtime-graph.js +89 -15
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +64 -491
- package/lib/git-defaults.js +56 -0
- package/lib/harness-capability-matrix.js +5 -5
- package/lib/hook-renderer.js +147 -16
- package/lib/insights.js +96 -80
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +44 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +292 -18
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/router.js +16 -1
- package/lib/memory-digest.js +47 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +212 -0
- package/lib/merge-rules.js +8 -4
- package/lib/npm-publish-workflow.js +272 -0
- package/lib/orientation.js +371 -49
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +9 -6
- package/lib/pr-monitor/journal.js +18 -2
- package/lib/pr-monitor/reconcile-executor.js +842 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +196 -0
- package/lib/pr-monitor/shepherd-lease.js +252 -0
- package/lib/pr-monitor/watch-lifecycle.js +14 -2
- package/lib/pr-pull.js +98 -24
- package/lib/pr-shepherd.js +34 -8
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +40 -0
- package/lib/protected-state-authority.js +305 -0
- package/lib/protected-state-surfaces.js +64 -44
- package/lib/release-readiness.js +51 -4
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/shell-utils.js +1 -1
- package/lib/skill-eval.js +750 -0
- package/lib/skills-sync.js +6 -3
- package/lib/smart-merge.js +28 -4
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +47 -9
- package/lib/using-forge.js +328 -0
- package/lib/workflow/enforce-stage.js +5 -5
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +6 -7
- package/rules/using-forge.md +24 -0
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +456 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +761 -0
- package/scripts/protected-state-check.js +47 -22
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +249 -37
- package/scripts/test.js +184 -44
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +10 -11
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +5 -11
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +84 -38
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +10 -12
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +92 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/beads-nudge.js +0 -91
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/commands/board.js +0 -64
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -192
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- package/scripts/forge-team/tests/dashboard.test.sh +0 -155
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require('node:crypto');
|
|
4
|
+
const { spawn } = require('node:child_process');
|
|
5
|
+
|
|
6
|
+
const { resolveKernelDatabasePath } = require('./kernel/cli-broker-factory');
|
|
7
|
+
const { createBuiltinSQLiteDriver } = require('./kernel/sqlite-driver');
|
|
8
|
+
const projectMemory = require('./project-memory');
|
|
9
|
+
|
|
10
|
+
const EVENT_TYPE = 'memory.recall.observed';
|
|
11
|
+
const OUTCOMES = new Set(['selected', 'empty', 'filtered', 'unsupported', 'timeout', 'error']);
|
|
12
|
+
const MAX_COUNT = 1_000_000;
|
|
13
|
+
const MAX_SELECTED_IDS = 20;
|
|
14
|
+
const MAX_MIX_KEYS = 10;
|
|
15
|
+
const EVENT_WRITER = "const { recordMemoryRecallPayload } = require(process.argv[1]); recordMemoryRecallPayload(process.argv[2], JSON.parse(process.argv[3])).catch(() => {});";
|
|
16
|
+
|
|
17
|
+
function boundedInteger(value) {
|
|
18
|
+
if (!Number.isFinite(value)) return 0;
|
|
19
|
+
return Math.min(MAX_COUNT, Math.max(0, Math.trunc(value)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function boundedString(value, maxLength) {
|
|
23
|
+
return String(value ?? '').slice(0, maxLength);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function boundedMix(mix) {
|
|
27
|
+
if (!mix || typeof mix !== 'object' || Array.isArray(mix)) return {};
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
Object.entries(mix)
|
|
30
|
+
.slice(0, MAX_MIX_KEYS)
|
|
31
|
+
.map(([label, count]) => [boundedString(label, 64), boundedInteger(count)])
|
|
32
|
+
.filter(([label, count]) => label && count > 0)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildRecallEventPayload(observation = {}) {
|
|
37
|
+
const selectedIds = Array.isArray(observation.selectedIds)
|
|
38
|
+
? observation.selectedIds
|
|
39
|
+
.slice(0, MAX_SELECTED_IDS)
|
|
40
|
+
.map(id => boundedString(id, 128))
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
: [];
|
|
43
|
+
return {
|
|
44
|
+
outcome: OUTCOMES.has(observation.outcome) ? observation.outcome : 'error',
|
|
45
|
+
counts: {
|
|
46
|
+
candidates: boundedInteger(observation.candidateCount),
|
|
47
|
+
eligible: boundedInteger(observation.eligibleCount),
|
|
48
|
+
selected: selectedIds.length,
|
|
49
|
+
},
|
|
50
|
+
selected_ids: selectedIds,
|
|
51
|
+
source_mix: boundedMix(observation.sourceMix),
|
|
52
|
+
trust_mix: boundedMix(observation.trustMix),
|
|
53
|
+
token_estimate: boundedInteger(observation.tokenEstimate),
|
|
54
|
+
elapsed_ms: boundedInteger(observation.elapsedMs),
|
|
55
|
+
harness: boundedString(observation.harness || 'unknown', 32),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeRecallEventPayload(payload = {}) {
|
|
60
|
+
return buildRecallEventPayload({
|
|
61
|
+
outcome: payload.outcome,
|
|
62
|
+
candidateCount: payload.counts?.candidates,
|
|
63
|
+
eligibleCount: payload.counts?.eligible,
|
|
64
|
+
selectedIds: payload.selected_ids,
|
|
65
|
+
sourceMix: payload.source_mix,
|
|
66
|
+
trustMix: payload.trust_mix,
|
|
67
|
+
tokenEstimate: payload.token_estimate,
|
|
68
|
+
elapsedMs: payload.elapsed_ms,
|
|
69
|
+
harness: payload.harness,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function recordMemoryRecallEvent(projectRoot, observation = {}, options = {}) {
|
|
74
|
+
return recordMemoryRecallPayload(projectRoot, buildRecallEventPayload(observation), options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function recordMemoryRecallPayload(projectRoot, payload, options = {}) {
|
|
78
|
+
let store = options.store;
|
|
79
|
+
let ownsStore = false;
|
|
80
|
+
try {
|
|
81
|
+
const safePayload = normalizeRecallEventPayload(payload);
|
|
82
|
+
const projectId = options.projectId || projectMemory.resolveProjectId(projectRoot, options);
|
|
83
|
+
if (!store) {
|
|
84
|
+
store = createBuiltinSQLiteDriver({
|
|
85
|
+
databasePath: resolveKernelDatabasePath({
|
|
86
|
+
projectRoot,
|
|
87
|
+
gitCommonDir: options.gitCommonDir,
|
|
88
|
+
databasePath: options.databasePath,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
ownsStore = true;
|
|
92
|
+
}
|
|
93
|
+
const id = (options.randomUUID || randomUUID)();
|
|
94
|
+
const createdAt = options.now || new Date().toISOString();
|
|
95
|
+
await store.insertKernelEvent({
|
|
96
|
+
id,
|
|
97
|
+
entity_type: 'project',
|
|
98
|
+
entity_id: projectId,
|
|
99
|
+
event_type: EVENT_TYPE,
|
|
100
|
+
idempotency_key: id,
|
|
101
|
+
expected_revision: 0,
|
|
102
|
+
actor: 'forge',
|
|
103
|
+
origin: 'hook',
|
|
104
|
+
payload: safePayload,
|
|
105
|
+
created_at: createdAt,
|
|
106
|
+
});
|
|
107
|
+
return { recorded: true, eventId: id };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
recorded: false,
|
|
111
|
+
reason: error && error.message ? error.message : 'telemetry unavailable',
|
|
112
|
+
};
|
|
113
|
+
} finally {
|
|
114
|
+
if (ownsStore && store && typeof store.close === 'function') {
|
|
115
|
+
try {
|
|
116
|
+
store.close();
|
|
117
|
+
} catch {
|
|
118
|
+
// Best-effort evidence must never block recall.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function launchMemoryRecallEvent(projectRoot, observation = {}, options = {}) {
|
|
125
|
+
try {
|
|
126
|
+
const child = (options.spawn || spawn)(
|
|
127
|
+
process.execPath,
|
|
128
|
+
['-e', EVENT_WRITER, __filename, projectRoot, JSON.stringify(buildRecallEventPayload(observation))],
|
|
129
|
+
{ detached: true, stdio: 'ignore', windowsHide: true },
|
|
130
|
+
);
|
|
131
|
+
if (child && typeof child.on === 'function') child.on('error', () => {});
|
|
132
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
133
|
+
return { launched: true };
|
|
134
|
+
} catch {
|
|
135
|
+
return { launched: false };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
EVENT_TYPE,
|
|
141
|
+
buildRecallEventPayload,
|
|
142
|
+
launchMemoryRecallEvent,
|
|
143
|
+
recordMemoryRecallEvent,
|
|
144
|
+
recordMemoryRecallPayload,
|
|
145
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { fenceUntrusted } = require('./untrusted-content');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @module memory-recall
|
|
7
|
+
*
|
|
8
|
+
* Pure selection core for the per-turn memory-recall hook (the query-relevant tier-2
|
|
9
|
+
* that complements the recency digest pushed at SessionStart). Kept free of stdin/fs so
|
|
10
|
+
* it is fully testable; lib/commands/hooks.js does the I/O wiring around it.
|
|
11
|
+
*
|
|
12
|
+
* Design constraints (verified against the Claude Code hooks contract + external memory
|
|
13
|
+
* research, kernel issue 781f6f65):
|
|
14
|
+
* - UserPromptSubmit additionalContext APPENDS to history every prompt, so a per-turn
|
|
15
|
+
* injector must stay tiny: a hard token budget, a relevance floor, and cross-turn
|
|
16
|
+
* dedupe. Below the bar -> inject NOTHING (silence is safe; a wrong memory at
|
|
17
|
+
* authority every turn is not).
|
|
18
|
+
* - Anaphora guard: a trivial query ("continue", "fix it") carries no retrieval signal,
|
|
19
|
+
* so ranking on it is worse than silence. Require a minimum of distinct content tokens.
|
|
20
|
+
* - Scope is a FILTER; relevance is the RANKER (bm25). Never sort by recency here — that
|
|
21
|
+
* is the recency digest's job, not tier-2's.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// A query needs at least this many distinct content tokens to be worth ranking on.
|
|
25
|
+
// Below it we treat the prompt as anaphora and inject nothing.
|
|
26
|
+
const MIN_QUERY_TOKENS = 2;
|
|
27
|
+
|
|
28
|
+
// Default token budget for the whole tier-2 injection. Deliberately small: it rides on
|
|
29
|
+
// EVERY prompt, and it must never starve the always-on SessionStart digest.
|
|
30
|
+
const DEFAULT_TOKEN_BUDGET = 400;
|
|
31
|
+
|
|
32
|
+
// Default relevance floor for the live hook path so it never runs floor-less. bm25 is
|
|
33
|
+
// more-negative-is-better, so 0 keeps every token-AND FTS match: the ACTIVE relevance gate
|
|
34
|
+
// today is the token-AND match plus the anaphora guard, and the numeric floor is a knob to
|
|
35
|
+
// be tightened (made negative) once shadow-logging measurement (781f6f65 step 0) shows where
|
|
36
|
+
// the corpus's relevant/irrelevant boundary sits. Named + wired so the default is explicit,
|
|
37
|
+
// not an accidental `undefined`.
|
|
38
|
+
const DEFAULT_SCORE_FLOOR = 0;
|
|
39
|
+
|
|
40
|
+
// Short/function words that carry no retrieval signal. Not exhaustive — just enough to
|
|
41
|
+
// stop pure anaphora ("do that now", "same for it") from clearing the guard.
|
|
42
|
+
const STOPWORDS = new Set([
|
|
43
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'for', 'to', 'of', 'in', 'on', 'at', 'by', 'is',
|
|
44
|
+
'it', 'this', 'that', 'these', 'those', 'do', 'did', 'now', 'then', 'same', 'again',
|
|
45
|
+
'continue', 'go', 'ok', 'okay', 'yes', 'no', 'fix', 'please', 'thanks', 'with', 'as',
|
|
46
|
+
'we', 'i', 'you', 'he', 'she', 'they', 'them', 'his', 'her', 'my', 'our', 'your',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// Rough token estimate: ~4 chars/token, matching lib/memory-digest.js's convention so
|
|
50
|
+
// the two tiers budget on the same scale.
|
|
51
|
+
function estimateTokens(text) {
|
|
52
|
+
return Math.ceil(String(text || '').length / 4);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeMemoryType(entry) {
|
|
56
|
+
const tags = Array.isArray(entry?.tags) ? entry.tags : [];
|
|
57
|
+
const tagged = tags.find(tag => /^type:[a-z0-9][a-z0-9-]{0,63}$/i.test(tag));
|
|
58
|
+
if (tagged) return tagged.slice('type:'.length).toLowerCase();
|
|
59
|
+
const category = entry?.value && typeof entry.value === 'object'
|
|
60
|
+
? entry.value.category
|
|
61
|
+
: null;
|
|
62
|
+
if (typeof category === 'string' && /^[a-z0-9][a-z0-9-]{0,63}$/i.test(category)) {
|
|
63
|
+
return category.toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
return entry?.value && typeof entry.value === 'object' ? 'machine-record' : 'note';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function memoryTrustStatus(entry) {
|
|
69
|
+
const tags = Array.isArray(entry?.tags) ? entry.tags : [];
|
|
70
|
+
const normalizedTags = tags.map(tag => String(tag).toLowerCase());
|
|
71
|
+
if (normalizedTags.includes('trust:confirmed')) return 'confirmed';
|
|
72
|
+
if (normalizedTags.some(tag => tag.startsWith('trust:')
|
|
73
|
+
|| tag === 'forge:auto-capture')) return 'suggested';
|
|
74
|
+
if (entry?.sourceAgent === 'forge remember (imported)') return 'suggested';
|
|
75
|
+
if (entry?.value && typeof entry.value === 'object') return 'suggested';
|
|
76
|
+
if (entry?.sourceAgent === 'forge remember' && typeof entry.value === 'string') return 'confirmed';
|
|
77
|
+
return 'suggested';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeRecallHit(entry, projectId) {
|
|
81
|
+
if (!entry) return null;
|
|
82
|
+
if (entry.memory_id) return entry;
|
|
83
|
+
const sourceRefs = Array.isArray(entry.beadsRefs) ? entry.beadsRefs : [];
|
|
84
|
+
return {
|
|
85
|
+
memory_id: entry.key,
|
|
86
|
+
type: normalizeMemoryType(entry),
|
|
87
|
+
content: typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value),
|
|
88
|
+
scope: !entry.scope || entry.scope === 'project' ? projectId : entry.scope,
|
|
89
|
+
trust_status: memoryTrustStatus(entry),
|
|
90
|
+
provenance: {
|
|
91
|
+
source_agent: entry.sourceAgent || '',
|
|
92
|
+
source_refs: sourceRefs,
|
|
93
|
+
},
|
|
94
|
+
updated_at: entry.timestamp,
|
|
95
|
+
score: entry.score,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse the JSON payload Claude Code delivers on a UserPromptSubmit hook's stdin. Never
|
|
101
|
+
* throws — any malformed input yields an empty prompt so the hook fails open.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} raw
|
|
104
|
+
* @returns {{ prompt: string, sessionId: (string|null) }}
|
|
105
|
+
*/
|
|
106
|
+
function parseHookInput(raw) {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(raw);
|
|
109
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
110
|
+
return { prompt: '', sessionId: null };
|
|
111
|
+
}
|
|
112
|
+
const prompt = typeof parsed.prompt === 'string' ? parsed.prompt : '';
|
|
113
|
+
const sessionId = typeof parsed.session_id === 'string' ? parsed.session_id : null;
|
|
114
|
+
return { prompt, sessionId };
|
|
115
|
+
} catch {
|
|
116
|
+
return { prompt: '', sessionId: null };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Distinct content tokens in a query — lowercased, length >= 3, minus stopwords. The
|
|
122
|
+
* anaphora guard counts these; the FTS layer does its own tokenization for the actual match.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} query
|
|
125
|
+
* @returns {string[]}
|
|
126
|
+
*/
|
|
127
|
+
function meaningfulTokens(query) {
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
// Unicode-aware split, matching the FTS tokenizer (/[\p{L}\p{N}]+/gu in the kernel driver)
|
|
130
|
+
// so non-Latin prompts (Cyrillic/CJK/accented) aren't silently stripped — otherwise the
|
|
131
|
+
// anaphora guard would disable recall for every non-Latin-script user.
|
|
132
|
+
for (const rawToken of String(query || '').toLowerCase().split(/[^\p{L}\p{N}]+/u)) {
|
|
133
|
+
if (!rawToken) continue;
|
|
134
|
+
if (STOPWORDS.has(rawToken)) continue;
|
|
135
|
+
// The length>=3 filter suppresses ASCII noise ("it", "do"), but CJK words are 1-2 chars
|
|
136
|
+
// and any non-ASCII token is inherently content — keep those regardless of length.
|
|
137
|
+
if (rawToken.length < 3 && /^[a-z0-9]+$/.test(rawToken)) continue;
|
|
138
|
+
seen.add(rawToken);
|
|
139
|
+
}
|
|
140
|
+
return [...seen];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Choose which memories to inject this turn. PURE.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} args
|
|
147
|
+
* @param {string} args.query — the submitted prompt
|
|
148
|
+
* @param {Array<{memory_id:string, content:string, score:number}>} args.hits — bm25-ordered
|
|
149
|
+
* (best/lowest score first), already relevance-only (token-AND matched)
|
|
150
|
+
* @param {number} [args.scoreFloor] — keep only hits with score <= floor (more negative =
|
|
151
|
+
* stronger). Omit/null to rely on the FTS match alone. The VALUE is corpus-dependent and
|
|
152
|
+
* should be tuned from shadow-logging measurement, not guessed — this is the knob.
|
|
153
|
+
* @param {number} [args.tokenBudget]
|
|
154
|
+
* @param {string[]} [args.excludeKeys] — keys injected on recent turns (cross-turn dedupe)
|
|
155
|
+
* @returns {{ lines: string[], injectedKeys: string[] }}
|
|
156
|
+
*/
|
|
157
|
+
function selectInjection({ query, hits, scoreFloor = null, tokenBudget = DEFAULT_TOKEN_BUDGET, excludeKeys = [] }) {
|
|
158
|
+
// Anaphora guard: a query with too little signal ranks garbage — stay silent.
|
|
159
|
+
if (meaningfulTokens(query).length < MIN_QUERY_TOKENS) {
|
|
160
|
+
return { lines: [], injectedKeys: [] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const exclude = new Set(excludeKeys || []);
|
|
164
|
+
const lines = [];
|
|
165
|
+
const entries = [];
|
|
166
|
+
const injectedKeys = [];
|
|
167
|
+
const usedSections = new Set();
|
|
168
|
+
let spent = 0;
|
|
169
|
+
|
|
170
|
+
for (const hit of hits || []) {
|
|
171
|
+
const memoryId = hit?.memory_id ?? hit?.key;
|
|
172
|
+
if (typeof memoryId !== 'string') continue;
|
|
173
|
+
if (exclude.has(memoryId)) continue;
|
|
174
|
+
// Relevance floor: below the bar contributes nothing. bm25 is more-negative-is-better.
|
|
175
|
+
if (typeof scoreFloor === 'number' && !(typeof hit.score === 'number' && hit.score <= scoreFloor)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const body = String((hit.content ?? hit.value) == null ? '' : (hit.content ?? hit.value));
|
|
179
|
+
const trust = hit.trust_status === 'confirmed' ? 'confirmed' : 'suggested';
|
|
180
|
+
const source = String(hit.provenance?.source_agent || hit.sourceAgent || 'unknown').slice(0, 80);
|
|
181
|
+
const updated = String(hit.updated_at || hit.timestamp || 'unknown').slice(0, 40);
|
|
182
|
+
const labeled = `[trust=${trust} source=${source} updated=${updated}] ${body}`;
|
|
183
|
+
const line = fenceUntrusted(labeled, { source: 'memory' });
|
|
184
|
+
const heading = trust === 'confirmed'
|
|
185
|
+
? 'Confirmed memory (project-local; provenance shown)'
|
|
186
|
+
: 'Suggested memory — verify before relying';
|
|
187
|
+
const cost = estimateTokens(line) + (usedSections.has(trust) ? 0 : estimateTokens(`${heading}\n`));
|
|
188
|
+
if (spent + cost > tokenBudget) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
lines.push(line);
|
|
192
|
+
entries.push({ trust, line });
|
|
193
|
+
injectedKeys.push(memoryId);
|
|
194
|
+
usedSections.add(trust);
|
|
195
|
+
spent += cost;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { lines, entries, injectedKeys };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
MIN_QUERY_TOKENS,
|
|
203
|
+
DEFAULT_TOKEN_BUDGET,
|
|
204
|
+
DEFAULT_SCORE_FLOOR,
|
|
205
|
+
estimateTokens,
|
|
206
|
+
memoryTrustStatus,
|
|
207
|
+
normalizeMemoryType,
|
|
208
|
+
normalizeRecallHit,
|
|
209
|
+
parseHookInput,
|
|
210
|
+
meaningfulTokens,
|
|
211
|
+
selectInjection,
|
|
212
|
+
};
|
package/lib/merge-rules.js
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* (squash | merge | rebase); and post-merge branch deletion.
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
|
-
const SUCCESS_CONCLUSIONS = new Set(['SUCCESS'
|
|
57
|
+
const SUCCESS_CONCLUSIONS = new Set(['SUCCESS']);
|
|
58
58
|
const MS_PER_MIN = 60_000;
|
|
59
59
|
|
|
60
60
|
/** Coerce an epoch-ms number, a Date, or an ISO string to epoch ms (NaN if unparseable). */
|
|
@@ -85,11 +85,15 @@ function toAccountList(arg) {
|
|
|
85
85
|
return [String(arg).trim().toLowerCase()];
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
/** A CI check is green
|
|
88
|
+
/** A CI check is green only when it is terminal and explicitly successful. */
|
|
89
89
|
function isCheckGreen(check) {
|
|
90
90
|
if (!check || typeof check !== 'object') return false;
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
if (Object.prototype.hasOwnProperty.call(check, 'state')
|
|
92
|
+
&& !Object.prototype.hasOwnProperty.call(check, 'status')) {
|
|
93
|
+
return String(check.state || '').toUpperCase() === 'SUCCESS';
|
|
94
|
+
}
|
|
95
|
+
return String(check.status || '').toUpperCase() === 'COMPLETED'
|
|
96
|
+
&& SUCCESS_CONCLUSIONS.has(String(check.conclusion || '').toUpperCase());
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
/** The most recent comment by timestamp; ties and unparseable stamps fall back to array order. */
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
createProtectedStateAuditRecord,
|
|
8
|
+
recordProtectedStateAuditEvent,
|
|
9
|
+
writeProtectedFile,
|
|
10
|
+
} = require('./protected-state-surfaces');
|
|
11
|
+
const {
|
|
12
|
+
issueNpmPublishWorkflowAuthorization,
|
|
13
|
+
} = require('./protected-state-authority');
|
|
14
|
+
|
|
15
|
+
const NPM_PUBLISH_WORKFLOW_PATH = '.github/workflows/npm-publish.yml';
|
|
16
|
+
|
|
17
|
+
function snapshotWorkflow(projectRoot) {
|
|
18
|
+
const fullPath = path.resolve(projectRoot, NPM_PUBLISH_WORKFLOW_PATH);
|
|
19
|
+
try {
|
|
20
|
+
const stat = fs.lstatSync(fullPath);
|
|
21
|
+
return {
|
|
22
|
+
fullPath,
|
|
23
|
+
existed: true,
|
|
24
|
+
content: stat.isFile() ? fs.readFileSync(fullPath) : null,
|
|
25
|
+
};
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT') return { fullPath, existed: false, content: null };
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rollbackWorkflow(snapshot, projectRoot, protectedWriter, writeOptions) {
|
|
33
|
+
if (!snapshot.existed) {
|
|
34
|
+
fs.rmSync(snapshot.fullPath, { force: true });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!Buffer.isBuffer(snapshot.content)) {
|
|
38
|
+
throw new Error('Previous workflow was not a regular file');
|
|
39
|
+
}
|
|
40
|
+
const restored = protectedWriter(
|
|
41
|
+
projectRoot,
|
|
42
|
+
NPM_PUBLISH_WORKFLOW_PATH,
|
|
43
|
+
snapshot.content,
|
|
44
|
+
writeOptions,
|
|
45
|
+
);
|
|
46
|
+
if (!restored.allowed) throw new Error(restored.reason || 'Protected workflow restore was denied');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderNpmPublishWorkflow() {
|
|
50
|
+
return `# Generated by \`forge release generate-npm-workflow\`. Do not edit directly.
|
|
51
|
+
name: Bun Package
|
|
52
|
+
|
|
53
|
+
on:
|
|
54
|
+
release:
|
|
55
|
+
types: [published]
|
|
56
|
+
|
|
57
|
+
permissions:
|
|
58
|
+
contents: read
|
|
59
|
+
|
|
60
|
+
jobs:
|
|
61
|
+
resolve-release:
|
|
62
|
+
name: Resolve immutable release SHA
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
permissions:
|
|
65
|
+
contents: read
|
|
66
|
+
outputs:
|
|
67
|
+
commitSha: \${{ steps.resolve.outputs.commitSha }}
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v7
|
|
70
|
+
with:
|
|
71
|
+
ref: \${{ github.event.release.tag_name }}
|
|
72
|
+
fetch-depth: 0
|
|
73
|
+
persist-credentials: false
|
|
74
|
+
- name: Resolve release tag
|
|
75
|
+
id: resolve
|
|
76
|
+
shell: bash
|
|
77
|
+
env:
|
|
78
|
+
RELEASE_TAG: \${{ github.event.release.tag_name }}
|
|
79
|
+
run: |
|
|
80
|
+
set -euo pipefail
|
|
81
|
+
COMMIT_SHA="$(git rev-parse "\${RELEASE_TAG}^{commit}")"
|
|
82
|
+
if [ -z "$COMMIT_SHA" ] || [ "$COMMIT_SHA" != "$(git rev-parse HEAD)" ]; then
|
|
83
|
+
echo "::error::Release tag did not resolve to the checked out commit"
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
echo "commitSha=$COMMIT_SHA" >> "$GITHUB_OUTPUT"
|
|
87
|
+
|
|
88
|
+
build:
|
|
89
|
+
needs: resolve-release
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
permissions:
|
|
92
|
+
contents: read
|
|
93
|
+
outputs:
|
|
94
|
+
commitSha: \${{ steps.evidence.outputs.commitSha }}
|
|
95
|
+
receipt: \${{ steps.evidence.outputs.receipt }}
|
|
96
|
+
receiptSubject: \${{ steps.evidence.outputs.receiptSubject }}
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v7
|
|
99
|
+
with:
|
|
100
|
+
ref: \${{ needs.resolve-release.outputs.commitSha }}
|
|
101
|
+
fetch-depth: 1
|
|
102
|
+
persist-credentials: false
|
|
103
|
+
- uses: actions/setup-node@v7
|
|
104
|
+
with:
|
|
105
|
+
node-version: 22
|
|
106
|
+
- uses: oven-sh/setup-bun@v2
|
|
107
|
+
with:
|
|
108
|
+
bun-version: 1.3.12
|
|
109
|
+
- run: bun install --frozen-lockfile
|
|
110
|
+
- name: Setup test fixtures
|
|
111
|
+
shell: bash
|
|
112
|
+
run: bash test-env/automation/setup-fixtures.sh --force
|
|
113
|
+
- name: Run complete release suite
|
|
114
|
+
shell: bash
|
|
115
|
+
run: |
|
|
116
|
+
set -euo pipefail
|
|
117
|
+
node scripts/test-full-suite.js --label-prefix release-full
|
|
118
|
+
- name: Emit attributable release suite receipt
|
|
119
|
+
id: evidence
|
|
120
|
+
env:
|
|
121
|
+
EXPECTED_SHA: \${{ needs.resolve-release.outputs.commitSha }}
|
|
122
|
+
run: node scripts/npm-release-receipt.js emit
|
|
123
|
+
|
|
124
|
+
publish-npm:
|
|
125
|
+
needs: [resolve-release, build]
|
|
126
|
+
runs-on: ubuntu-latest
|
|
127
|
+
permissions:
|
|
128
|
+
contents: read
|
|
129
|
+
id-token: write
|
|
130
|
+
steps:
|
|
131
|
+
- uses: actions/checkout@v7
|
|
132
|
+
with:
|
|
133
|
+
ref: \${{ needs.resolve-release.outputs.commitSha }}
|
|
134
|
+
fetch-depth: 1
|
|
135
|
+
persist-credentials: false
|
|
136
|
+
- uses: actions/setup-node@v7
|
|
137
|
+
with:
|
|
138
|
+
node-version: 22
|
|
139
|
+
registry-url: https://registry.npmjs.org/
|
|
140
|
+
- name: Verify exact release suite receipt
|
|
141
|
+
env:
|
|
142
|
+
EXPECTED_SHA: \${{ needs.resolve-release.outputs.commitSha }}
|
|
143
|
+
VERIFIED_SHA: \${{ needs.build.outputs.commitSha }}
|
|
144
|
+
RECEIPT: \${{ needs.build.outputs.receipt }}
|
|
145
|
+
RECEIPT_SUBJECT: \${{ needs.build.outputs.receiptSubject }}
|
|
146
|
+
run: node scripts/npm-release-receipt.js verify
|
|
147
|
+
- uses: oven-sh/setup-bun@v2
|
|
148
|
+
with:
|
|
149
|
+
bun-version: 1.3.12
|
|
150
|
+
- run: bun install --frozen-lockfile
|
|
151
|
+
- name: Release tag matches package.json version
|
|
152
|
+
shell: bash
|
|
153
|
+
run: |
|
|
154
|
+
set -euo pipefail
|
|
155
|
+
PKG="$(node -p "require('./package.json').version")"
|
|
156
|
+
TAG="\${GITHUB_REF_NAME#v}"
|
|
157
|
+
if [ "$PKG" != "$TAG" ]; then
|
|
158
|
+
echo "::error::Release tag ($TAG) != package.json version ($PKG). Bump package.json before releasing."
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
- name: Release-readiness gate
|
|
162
|
+
run: node bin/forge.js release check --target "$(node -p "require('./package.json').version.split('-')[0]")"
|
|
163
|
+
- name: Verify package contents
|
|
164
|
+
run: npm pack --dry-run
|
|
165
|
+
- name: Ensure OIDC-capable npm
|
|
166
|
+
run: npm install --global npm@11.5.1 && npm --version
|
|
167
|
+
- name: Publish to npm
|
|
168
|
+
shell: bash
|
|
169
|
+
run: |
|
|
170
|
+
set -euo pipefail
|
|
171
|
+
VERSION="$(node -p "require('./package.json').version")"
|
|
172
|
+
if echo "$VERSION" | grep -q '-'; then
|
|
173
|
+
echo "Prerelease $VERSION -> publishing under the beta dist-tag"
|
|
174
|
+
npm publish --provenance --tag beta
|
|
175
|
+
else
|
|
176
|
+
npm publish --provenance
|
|
177
|
+
fi
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function generateNpmPublishWorkflow(projectRoot, options = {}) {
|
|
182
|
+
const content = renderNpmPublishWorkflow();
|
|
183
|
+
const actor = options.actor || options.env?.FORGE_ACTOR || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown';
|
|
184
|
+
const protectedWriter = options.writeProtectedFile || writeProtectedFile;
|
|
185
|
+
const auditWriter = options.recordProtectedStateAuditEvent || recordProtectedStateAuditEvent;
|
|
186
|
+
const authorizationWriter = options.issueNpmPublishWorkflowAuthorization || issueNpmPublishWorkflowAuthorization;
|
|
187
|
+
const snapshot = snapshotWorkflow(projectRoot);
|
|
188
|
+
const writeOptions = {
|
|
189
|
+
actor,
|
|
190
|
+
operation: 'generate_npm_workflow',
|
|
191
|
+
viaForgeApi: true,
|
|
192
|
+
surface: 'workflows',
|
|
193
|
+
};
|
|
194
|
+
let write;
|
|
195
|
+
try {
|
|
196
|
+
write = protectedWriter(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, content, writeOptions);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return { success: false, error: error.message };
|
|
199
|
+
}
|
|
200
|
+
if (!write.allowed) {
|
|
201
|
+
return { success: false, error: write.reason, write };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const auditRecord = createProtectedStateAuditRecord({
|
|
205
|
+
actor,
|
|
206
|
+
surface: 'workflows',
|
|
207
|
+
path: NPM_PUBLISH_WORKFLOW_PATH,
|
|
208
|
+
content,
|
|
209
|
+
operation: 'generate_npm_workflow',
|
|
210
|
+
});
|
|
211
|
+
let audit;
|
|
212
|
+
try {
|
|
213
|
+
audit = auditWriter(auditRecord, { cwd: projectRoot });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
audit = { success: false, error: error.message };
|
|
216
|
+
}
|
|
217
|
+
if (!audit.success) {
|
|
218
|
+
let rollbackError;
|
|
219
|
+
try {
|
|
220
|
+
rollbackWorkflow(snapshot, projectRoot, protectedWriter, writeOptions);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
rollbackError = error.message;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
error: `Could not record protected-state authorization: ${audit.error}`,
|
|
227
|
+
audit,
|
|
228
|
+
rollbackError,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let trustedAuthorization;
|
|
233
|
+
try {
|
|
234
|
+
trustedAuthorization = await authorizationWriter(
|
|
235
|
+
projectRoot,
|
|
236
|
+
{ actor },
|
|
237
|
+
{ deps: options.kernelDeps },
|
|
238
|
+
);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
trustedAuthorization = { success: false, error: error.message };
|
|
241
|
+
}
|
|
242
|
+
if (!trustedAuthorization.success) {
|
|
243
|
+
let rollbackError;
|
|
244
|
+
try {
|
|
245
|
+
rollbackWorkflow(snapshot, projectRoot, protectedWriter, writeOptions);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
rollbackError = error.message;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
success: false,
|
|
251
|
+
error: `Could not record trusted protected-state authorization: ${trustedAuthorization.error}`,
|
|
252
|
+
audit,
|
|
253
|
+
trustedAuthorization,
|
|
254
|
+
rollbackError,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
success: true,
|
|
260
|
+
path: NPM_PUBLISH_WORKFLOW_PATH,
|
|
261
|
+
contentHash: write.contentHash,
|
|
262
|
+
write,
|
|
263
|
+
audit,
|
|
264
|
+
trustedAuthorization,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
NPM_PUBLISH_WORKFLOW_PATH,
|
|
270
|
+
renderNpmPublishWorkflow,
|
|
271
|
+
generateNpmPublishWorkflow,
|
|
272
|
+
};
|