forge-workflow 0.1.0-beta.2 → 0.1.0-beta.4
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/.forge/hooks/check-tdd.js +79 -5
- package/.forge/hooks/forge-native-hook.js +194 -8
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +28 -0
- package/QUICKSTART.md +6 -2
- package/README.md +3 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +95 -19
- package/docs/guides/SETUP.md +4 -1
- package/docs/guides/SUPPORT.md +5 -0
- package/docs/reference/COMMANDS.md +12 -1
- package/docs/reference/shepherd.md +114 -3
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapters/beads-kernel-compat.js +67 -0
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/adoption-profiles.js +17 -4
- package/lib/agents-config.js +5 -0
- package/lib/beads-detect.js +60 -0
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +65 -41
- package/lib/commands/_manifest.js +3 -1
- package/lib/commands/_registry.js +14 -0
- package/lib/commands/_resolve-command-opts.js +36 -60
- package/lib/commands/claim.js +2 -4
- package/lib/commands/gate.js +19 -2
- package/lib/commands/hooks.js +294 -12
- package/lib/commands/init.js +26 -20
- package/lib/commands/memory.js +81 -0
- package/lib/commands/migrate.js +0 -161
- package/lib/commands/plan.js +71 -123
- package/lib/commands/pr.js +88 -0
- package/lib/commands/prime.js +8 -1
- package/lib/commands/push.js +66 -0
- package/lib/commands/recall.js +67 -12
- package/lib/commands/recap.js +18 -4
- package/lib/commands/release.js +15 -3
- package/lib/commands/remember.js +86 -20
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +135 -73
- package/lib/commands/shepherd.js +102 -4
- package/lib/commands/ship.js +40 -4
- package/lib/commands/skill.js +275 -0
- package/lib/commands/status.js +37 -32
- package/lib/commands/test.js +32 -0
- package/lib/commands/worktree.js +87 -5
- package/lib/core/runtime-graph.js +122 -17
- package/lib/forge-issues.js +13 -464
- package/lib/gate-events.js +54 -55
- package/lib/global-flags.js +30 -0
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +144 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +7 -1
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +187 -1
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/digest.js +206 -0
- package/lib/pr-monitor/reconcile-executor.js +659 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-sticky.js +57 -8
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +113 -17
- package/lib/project-memory.js +8 -0
- package/lib/release-readiness.js +17 -1
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/skill-eval.js +750 -0
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/upgrade-safety.js +52 -1
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +64 -7
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +2 -2
- package/rules/using-forge.md +24 -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/pr-auto-actions.js +93 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/test.js +8 -1
- 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 +6 -0
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +1 -0
- 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 +76 -36
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +6 -0
- 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 +87 -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/commands/board.js +0 -64
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- package/scripts/forge-team/tests/dashboard.test.sh +0 -155
|
@@ -161,6 +161,55 @@ const PLAN_SUBSKILL_DEFINITIONS = [
|
|
|
161
161
|
|
|
162
162
|
const PLAN_SUBSKILL_IDS = new Set(PLAN_SUBSKILL_DEFINITIONS.map(definition => definition.id));
|
|
163
163
|
|
|
164
|
+
// The `smith` orchestrator composes the stage skills end-to-end. Its sub-skills
|
|
165
|
+
// are the stage skill NAMES (each a real `skills/<name>/` dir), unlike plan's
|
|
166
|
+
// fine-grained internal planning phases. Both resolve through the ONE generic
|
|
167
|
+
// registry below (keyed by owning skill), so composition is no longer hardcoded
|
|
168
|
+
// to plan. See docs/work kernel-native-skills composition epic (a0776e61) +
|
|
169
|
+
// smith-orchestrator (7da81cbd).
|
|
170
|
+
const SMITH_SUBSKILL_DEFINITIONS = ['plan', 'dev', 'validate', 'ship', 'review', 'verify']
|
|
171
|
+
.map(stage => ({ id: stage, label: `${stage} stage`, owner: 'smith' }));
|
|
172
|
+
|
|
173
|
+
// Skills that `plan` COMPOSES at the whole-skill level (its SKILL.md `subskills:`
|
|
174
|
+
// frontmatter), distinct from the fine-grained plan.* planning micro-phases in
|
|
175
|
+
// PLAN_SUBSKILL_DEFINITIONS. A composed whole-skill is an advertised sub-skill
|
|
176
|
+
// that must both VALIDATE and RESOLVE, but it is NOT a partialInvocation
|
|
177
|
+
// micro-phase (it maps to no runtime-graph action), so it lives OUTSIDE
|
|
178
|
+
// PLAN_SUBSKILL_IDS (which governs planning.template.only/skip).
|
|
179
|
+
const PLAN_COMPOSED_SUBSKILLS = ['research'];
|
|
180
|
+
const PLAN_COMPOSED_SUBSKILL_DEFINITIONS = PLAN_COMPOSED_SUBSKILLS.map(id => ({
|
|
181
|
+
id,
|
|
182
|
+
label: `${id.charAt(0).toUpperCase()}${id.slice(1)} (composed skill)`,
|
|
183
|
+
kind: 'composed-skill',
|
|
184
|
+
owner: 'plan',
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
// Generic per-skill sub-skill registry, keyed by owning skill id. Generalizes
|
|
188
|
+
// plan's previously hardcoded PLAN_SUBSKILL_DEFINITIONS / validatePlanSubSkillList
|
|
189
|
+
// so any composing skill (plan, smith, ...) declares its sub-skill set through a
|
|
190
|
+
// single mechanism. This registry is the SINGLE source for BOTH validation (ids)
|
|
191
|
+
// and resolution (definitions), so getSubSkillDefinitions and validateSubSkillList
|
|
192
|
+
// can never disagree. plan's set = its planning micro-phases + composed whole-skills.
|
|
193
|
+
const SUBSKILL_REGISTRY = {
|
|
194
|
+
plan: [...PLAN_SUBSKILL_DEFINITIONS, ...PLAN_COMPOSED_SUBSKILL_DEFINITIONS],
|
|
195
|
+
smith: SMITH_SUBSKILL_DEFINITIONS,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Validation id sets DERIVED from the registry definitions — guarantees
|
|
199
|
+
// validate-vs-resolve parity for every owner (a subskill that validates also
|
|
200
|
+
// resolves, and vice versa).
|
|
201
|
+
const SUBSKILL_IDS_BY_OWNER = Object.fromEntries(
|
|
202
|
+
Object.entries(SUBSKILL_REGISTRY).map(([owner, defs]) => [owner, new Set(defs.map(d => d.id))])
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
function getSubSkillDefinitions(owner) {
|
|
206
|
+
return SUBSKILL_REGISTRY[owner] ? [...SUBSKILL_REGISTRY[owner]] : [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getSubSkillIds(owner) {
|
|
210
|
+
return new Set(SUBSKILL_IDS_BY_OWNER[owner] || []);
|
|
211
|
+
}
|
|
212
|
+
|
|
164
213
|
function planningSubSkillFromDefinition(definition) {
|
|
165
214
|
return PlanningSubSkill({
|
|
166
215
|
id: definition.id,
|
|
@@ -412,6 +461,27 @@ const RESOLVED_RUNTIME_GRAPH = {
|
|
|
412
461
|
label: 'Issue write verification (check-after-write)',
|
|
413
462
|
requires: [],
|
|
414
463
|
}),
|
|
464
|
+
// Grounding gates (epic 6ef96e92, design docs/work/2026-07-16-grounding-
|
|
465
|
+
// enforcement/design.md) — the first gates that DENY (fd4c03b3's first real
|
|
466
|
+
// payment). gate.read_first: acting on an issue requires having read it —
|
|
467
|
+
// consulted fail-closed at the `forge claim` boundary against a
|
|
468
|
+
// `context.loaded` kernel event (lib/grounding/context-events.js), remedy
|
|
469
|
+
// `forge recap <id>`. gate.cite: shipped artifacts must cite their sources —
|
|
470
|
+
// registered here (togglable) but its scanner lands in P3; it denies nothing
|
|
471
|
+
// yet. Both phase-less, default-ON, UNLOCKED, same toggle surface as
|
|
472
|
+
// gate.issue_verify (`forge gate disable gate.read_first`). Master switch is
|
|
473
|
+
// the unlocked rail.grounding below. `requires: []` — the context.loaded
|
|
474
|
+
// event (not evidence) is the exit condition.
|
|
475
|
+
Gate({
|
|
476
|
+
id: 'gate.read_first',
|
|
477
|
+
label: 'Read the issue before acting on it',
|
|
478
|
+
requires: [],
|
|
479
|
+
}),
|
|
480
|
+
Gate({
|
|
481
|
+
id: 'gate.cite',
|
|
482
|
+
label: 'Cite sources in shipped artifacts',
|
|
483
|
+
requires: [],
|
|
484
|
+
}),
|
|
415
485
|
],
|
|
416
486
|
evidence: [
|
|
417
487
|
Evidence({
|
|
@@ -446,12 +516,18 @@ const RESOLVED_RUNTIME_GRAPH = {
|
|
|
446
516
|
// may opt out via `forge gate disable rail.kernel_tracking`, consumed by the
|
|
447
517
|
// resolver's rail-aware gate loop over workflow.gates.<id>.enabled).
|
|
448
518
|
rails: [
|
|
449
|
-
{ key: 'tdd_intent', label: 'TDD intent evidence', description: 'Source changes require test intent and TDD evidence.' },
|
|
519
|
+
{ key: 'tdd_intent', label: 'TDD intent evidence', description: 'Source changes require test intent and TDD evidence. Strong default (ON), but not a hard floor — disable with `forge gate disable rail.tdd_intent`.', locked: false },
|
|
450
520
|
{ key: 'secret_scan', label: 'Secret scan', description: 'Validation must not knowingly ship secrets.' },
|
|
451
521
|
{ key: 'branch_protection', label: 'Branch protection', description: 'Ship through reviewed branches instead of direct protected-branch edits.' },
|
|
452
522
|
{ key: 'signed_commits', label: 'Signed commits', description: 'Preserve commit provenance requirements where configured.' },
|
|
453
523
|
{ key: 'schema_integrity', label: 'Schema integrity', description: 'Keep runtime graph and config schemas internally consistent.' },
|
|
454
524
|
{ key: 'kernel_tracking', label: 'Kernel issue tracking', description: 'Every issue, idea, bug, and decision discussed is filed to the Forge Kernel.', locked: false },
|
|
525
|
+
{ key: 'auto_shepherd', label: 'Auto-start PR shepherd watch', description: 'On `forge ship` success, auto-start the detached, self-stopping `forge shepherd watch <pr>` monitor so a shipped PR is tended without a manual trigger. Default-ON, UNLOCKED — opt out with `forge gate disable rail.auto_shepherd`.', locked: false },
|
|
526
|
+
// grounding — the one-switch master toggle over gate.read_first + gate.cite.
|
|
527
|
+
// UNLOCKED, default-ON (like kernel_tracking): a maintainer may opt out via
|
|
528
|
+
// `forge gate disable rail.grounding`, which the read_first consult treats as
|
|
529
|
+
// "allow, logged". Ingrains "read the documented source before acting; cite it".
|
|
530
|
+
{ key: 'grounding', label: 'Documented-grounding enforcement', description: 'Read the issue before acting on it; cite sources in shipped artifacts.', locked: false },
|
|
455
531
|
].map(def => Rail({ id: `rail.${def.key}`, ...def })),
|
|
456
532
|
adapters: [
|
|
457
533
|
Adapter({
|
|
@@ -605,14 +681,18 @@ function applyRailConfig(graph, config, errors) {
|
|
|
605
681
|
if (!enabled.valid) {
|
|
606
682
|
continue;
|
|
607
683
|
}
|
|
608
|
-
if (enabled.value === false) {
|
|
684
|
+
if (enabled.value === false && rail.locked === true) {
|
|
685
|
+
// Locked L1 rails remain a non-negotiable floor; only unlocked rails
|
|
686
|
+
// (e.g. tdd_intent, kernel_tracking) may be disabled via config.
|
|
609
687
|
errors.push({
|
|
610
688
|
code: 'LOCKED_L1_RAIL_DISABLED',
|
|
611
689
|
message: `Cannot disable locked L1 rail '${key}'.`,
|
|
612
690
|
});
|
|
613
691
|
continue;
|
|
614
692
|
}
|
|
615
|
-
if (
|
|
693
|
+
if (enabled.present) {
|
|
694
|
+
markConfigured(rail, { enabled: enabled.value });
|
|
695
|
+
} else if (options && Object.keys(options).length > 0) {
|
|
616
696
|
rail.configSource = CONFIG_SOURCE;
|
|
617
697
|
}
|
|
618
698
|
}
|
|
@@ -746,31 +826,51 @@ function assertStringList(value, errors, code, message) {
|
|
|
746
826
|
return valid;
|
|
747
827
|
}
|
|
748
828
|
|
|
749
|
-
function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
829
|
+
function subSkillCodesForOwner(owner) {
|
|
830
|
+
return owner === 'plan'
|
|
831
|
+
? { listCode: 'INVALID_PLAN_SUBSKILL_LIST', unknownCode: 'UNKNOWN_PLAN_SUBSKILL', noun: 'planning sub-skill' }
|
|
832
|
+
: { listCode: 'INVALID_SUBSKILL_LIST', unknownCode: 'UNKNOWN_SUBSKILL', noun: `${owner} sub-skill` };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Core list validator against an explicit known-id Set. The two callers below
|
|
836
|
+
// deliberately pass DIFFERENT id sets so the two contracts never leak into each
|
|
837
|
+
// other (see validatePlanSubSkillList).
|
|
838
|
+
function validateAgainstKnownIds(knownIds, codes, value, errors, path) {
|
|
839
|
+
const { listCode, unknownCode, noun } = codes;
|
|
840
|
+
if (!assertStringList(value, errors, listCode, `${path} must be a list of ${noun} IDs.`)) {
|
|
756
841
|
return undefined;
|
|
757
842
|
}
|
|
758
843
|
|
|
759
844
|
const normalized = value.map(item => item.trim());
|
|
760
845
|
let hasUnknown = false;
|
|
761
846
|
for (const id of normalized) {
|
|
762
|
-
if (!
|
|
847
|
+
if (!knownIds.has(id)) {
|
|
763
848
|
hasUnknown = true;
|
|
764
849
|
errors.push({
|
|
765
|
-
code:
|
|
766
|
-
message: `Unknown
|
|
850
|
+
code: unknownCode,
|
|
851
|
+
message: `Unknown ${noun} '${id}' in ${CONFIG_SOURCE}.`,
|
|
767
852
|
});
|
|
768
853
|
}
|
|
769
854
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
855
|
+
return hasUnknown ? undefined : normalized;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Generic COMPOSITION validator, keyed by owning skill — validates a skill's
|
|
859
|
+
// ADVERTISED (SKILL.md frontmatter) subskills. For `plan` the known-id set is the
|
|
860
|
+
// composition union (plan.* micro-phases + composed whole-skills like research).
|
|
861
|
+
function validateSubSkillList(owner, value, errors, path) {
|
|
862
|
+
const knownIds = SUBSKILL_IDS_BY_OWNER[owner] || new Set();
|
|
863
|
+
return validateAgainstKnownIds(knownIds, subSkillCodesForOwner(owner), value, errors, path);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// partialInvocation validator (.forge/config.yaml planning.template.only/skip).
|
|
867
|
+
// SEPARATE contract from composition: it MUST accept ONLY the fine-grained plan.*
|
|
868
|
+
// MICRO-PHASE ids (PLAN_SUBSKILL_IDS), never composed whole-skill ids like
|
|
869
|
+
// `research` — a composed skill maps to no runtime-graph action, so `only:
|
|
870
|
+
// [research]` would be silent dead config. Validates against PLAN_SUBSKILL_IDS,
|
|
871
|
+
// NOT the composition union, so it fails closed on composed ids.
|
|
872
|
+
function validatePlanSubSkillList(value, errors, path) {
|
|
873
|
+
return validateAgainstKnownIds(PLAN_SUBSKILL_IDS, subSkillCodesForOwner('plan'), value, errors, path);
|
|
774
874
|
}
|
|
775
875
|
|
|
776
876
|
function applyPlanningMode(template, nextTemplate, errors) {
|
|
@@ -937,6 +1037,11 @@ module.exports = {
|
|
|
937
1037
|
PlanningSubSkill,
|
|
938
1038
|
Role,
|
|
939
1039
|
ROLE_IDS,
|
|
1040
|
+
SUBSKILL_REGISTRY,
|
|
1041
|
+
getSubSkillDefinitions,
|
|
1042
|
+
getSubSkillIds,
|
|
1043
|
+
validateSubSkillList,
|
|
1044
|
+
validatePlanSubSkillList,
|
|
940
1045
|
loadRuntimeGraphConfig,
|
|
941
1046
|
lintRuntimeGraphConfig,
|
|
942
1047
|
resolveRuntimeGraph,
|